Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions frontend/src/components/OnboardingSteps/AvatarSelectionStep.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import {
setAvatar,
setName,
Expand All @@ -18,11 +18,12 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { avatars } from '@/constants/avatars';
import { AppFeatures } from '@/components/OnboardingSteps/AppFeatures';
import { RootState } from '@/app/store';

interface AvatarNameSelectionStepProps {
stepIndex: number;
totalSteps: number;
currentStepDisplayIndex: number;
currentStepDisplayIndex?: number;
}

export const AvatarSelectionStep: React.FC<AvatarNameSelectionStepProps> = ({
Expand All @@ -32,14 +33,26 @@ export const AvatarSelectionStep: React.FC<AvatarNameSelectionStepProps> = ({
}) => {
const dispatch = useDispatch();

const [name, setLocalName] = useState('');
const [selectedAvatar, setLocalAvatar] = useState('');
const displayIndex = currentStepDisplayIndex ?? stepIndex;

const [name, setLocalName] = useState(localStorage.getItem('name') || '');
const [selectedAvatar, setLocalAvatar] = useState(
localStorage.getItem('avatar') || '',
);

const isEditing = useSelector(
(state: RootState) => state.onboarding.isEditing,
);

useEffect(() => {
if (localStorage.getItem('name') && localStorage.getItem('avatar')) {
if (
localStorage.getItem('name') &&
localStorage.getItem('avatar') &&
!isEditing
) {
dispatch(markCompleted(stepIndex));
}
}, []);
}, [dispatch, isEditing, stepIndex]);

const handleAvatarSelect = (avatar: string) => {
setLocalAvatar(avatar);
Expand All @@ -57,7 +70,11 @@ export const AvatarSelectionStep: React.FC<AvatarNameSelectionStepProps> = ({
dispatch(markCompleted(stepIndex));
};

if (localStorage.getItem('name') && localStorage.getItem('avatar')) {
if (
localStorage.getItem('name') &&
localStorage.getItem('avatar') &&
!isEditing
) {
return null;
}

Expand All @@ -67,20 +84,20 @@ export const AvatarSelectionStep: React.FC<AvatarNameSelectionStepProps> = ({
<CardHeader className="p-3">
<div className="text-muted-foreground mb-1 flex justify-between text-xs">
<span>
Step {currentStepDisplayIndex + 1} of {totalSteps}
</span>
<span>
{Math.round(((currentStepDisplayIndex + 1) / totalSteps) * 100)}%
Step {displayIndex + 1} of {totalSteps}
</span>
<span>{Math.round(((displayIndex + 1) / totalSteps) * 100)}%</span>
</div>

<div className="bg-muted mb-2 h-1.5 w-full rounded-full">
<div
className="bg-primary h-full rounded-full transition-all duration-300"
style={{
width: `${((currentStepDisplayIndex + 1) / totalSteps) * 100}%`,
width: `${((displayIndex + 1) / totalSteps) * 100}%`,
}}
/>
</div>

<CardTitle className="mt-1 text-xl font-semibold">
Welcome to PictoPy
</CardTitle>
Expand All @@ -103,7 +120,6 @@ export const AvatarSelectionStep: React.FC<AvatarNameSelectionStepProps> = ({
/>
</div>

{/* Avatar Grid */}
<div className="mb-5">
<Label className="mb-2 block text-sm">Choose Your Avatar</Label>
<div className="grid grid-cols-4 gap-3">
Expand Down
36 changes: 23 additions & 13 deletions frontend/src/components/OnboardingSteps/FolderSetupStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ import {
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { FolderOpen, X, Folder } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { AppDispatch } from '@/app/store';
import { markCompleted, previousStep } from '@/features/onboardingSlice';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '@/app/store';
import {
markCompleted,
previousStep,
setIsEditing,
} from '@/features/onboardingSlice';
import { AppFeatures } from '@/components/OnboardingSteps/AppFeatures';
import { useFolder } from '@/hooks/useFolder';
import { useEffect, useState } from 'react';

interface FolderSetupStepProps {
stepIndex: number;
totalSteps: number;
currentStepDisplayIndex: number;
currentStepDisplayIndex?: number;
}

export function FolderSetupStep({
Expand All @@ -29,14 +33,18 @@ export function FolderSetupStep({
}: FolderSetupStepProps) {
const dispatch = useDispatch<AppDispatch>();

// Local state for folders
const displayIndex = currentStepDisplayIndex ?? stepIndex;

const [folder, setFolder] = useState<string>('');
const isEditing = useSelector(
(state: RootState) => state.onboarding.isEditing,
);

useEffect(() => {
if (localStorage.getItem('folderChosen') === 'true') {
if (localStorage.getItem('folderChosen') === 'true' && !isEditing) {
dispatch(markCompleted(stepIndex));
}
}, []);
}, [dispatch, isEditing, stepIndex]);

const { pickSingleFolder, addFolderMutate } = useFolder({
title: 'Select folder to import photos from',
Expand All @@ -60,26 +68,27 @@ export function FolderSetupStep({
};

const handleBack = () => {
dispatch(setIsEditing(true));
dispatch(previousStep());
};

if (localStorage.getItem('folderChosen') === 'true') {
if (localStorage.getItem('folderChosen') === 'true' && !isEditing) {
return null;
}
const progressPercent = Math.round(
((currentStepDisplayIndex + 1) / totalSteps) * 100,
);

const progressPercent = Math.round(((displayIndex + 1) / totalSteps) * 100);

return (
<>
<Card className="flex max-h-full w-1/2 flex-col border p-4">
<CardHeader className="p-3">
<div className="text-muted-foreground mb-1 flex justify-between text-xs">
<span>
Step {currentStepDisplayIndex + 1} of {totalSteps}
Step {displayIndex + 1} of {totalSteps}
</span>
<span>{progressPercent}%</span>
</div>

<div className="bg-muted mb-4 h-2 w-full rounded-full">
<div
className="bg-primary h-full rounded-full transition-all duration-300"
Expand All @@ -94,6 +103,7 @@ export function FolderSetupStep({
Choose the folder you want to import your photos from
</CardDescription>
</CardHeader>

<CardContent className="flex-1 space-y-6 overflow-y-auto p-1 px-2">
{!folder && (
<div
Expand Down Expand Up @@ -128,7 +138,7 @@ export function FolderSetupStep({
</div>
<button
type="button"
onClick={() => handleRemoveFolder()}
onClick={handleRemoveFolder}
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 ml-2 flex h-7 w-7 items-center justify-center rounded-md opacity-0 transition-colors group-hover:opacity-100"
>
<X className="h-4 w-4" />
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/components/OnboardingSteps/OnboardingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ServerCheck } from './ServerCheck';
interface OnboardingStepProps {
stepIndex: number;
stepName: string;
currentStepDisplayIndex?: number;
}

const VISIBLE_STEPS = [
Expand All @@ -22,17 +23,20 @@ const VISIBLE_STEPS = [
export const OnboardingStep: React.FC<OnboardingStepProps> = ({
stepIndex,
stepName,
currentStepDisplayIndex,
}) => {
const visibleStepIndex = VISIBLE_STEPS.indexOf(stepName);
const STEP_NAMES = Object.values(STEPS);
const safeIndex = Math.max(0, Math.min(stepIndex, STEP_NAMES.length - 1));
const currentStepName = stepName || STEP_NAMES[safeIndex];

const sharedProps = {
stepIndex,
stepIndex: safeIndex,
totalSteps: VISIBLE_STEPS.length,
currentStepDisplayIndex: visibleStepIndex,
currentStepDisplayIndex,
};

const renderStepComponent = () => {
switch (stepName) {
switch (currentStepName) {
case STEPS.AVATAR_SELECTION_STEP:
return <AvatarSelectionStep {...sharedProps} />;
case STEPS.FOLDER_SETUP_STEP:
Expand Down
38 changes: 27 additions & 11 deletions frontend/src/components/OnboardingSteps/ThemeSelectionStep.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { AppDispatch } from '@/app/store';
import { markCompleted, previousStep } from '@/features/onboardingSlice';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from '@/app/store';
import {
markCompleted,
previousStep,
setIsEditing,
} from '@/features/onboardingSlice';

import { Button } from '@/components/ui/button';
import {
Expand All @@ -18,10 +22,11 @@ import { Sun, Moon, Monitor } from 'lucide-react';

import { AppFeatures } from '@/components/OnboardingSteps/AppFeatures';
import { useTheme } from '@/contexts/ThemeContext';

interface ThemeSelectionStepProps {
stepIndex: number;
totalSteps: number;
currentStepDisplayIndex: number;
currentStepDisplayIndex?: number;
}

export const ThemeSelectionStep: React.FC<ThemeSelectionStepProps> = ({
Expand All @@ -31,12 +36,19 @@ export const ThemeSelectionStep: React.FC<ThemeSelectionStepProps> = ({
}) => {
const { setTheme, theme } = useTheme();
const dispatch = useDispatch<AppDispatch>();
const isEditing = useSelector(
(state: RootState) => state.onboarding.isEditing,
);

// ✅ TS-safe fallback ONLY (no logic change)
const displayIndex = currentStepDisplayIndex ?? stepIndex;

useEffect(() => {
if (localStorage.getItem('themeChosen')) {
if (localStorage.getItem('themeChosen') === 'true' && !isEditing) {
dispatch(markCompleted(stepIndex));
}
}, []);
}, [dispatch, isEditing, stepIndex]);

const handleThemeChange = (value: 'light' | 'dark' | 'system') => {
setTheme(value);
};
Expand All @@ -47,25 +59,27 @@ export const ThemeSelectionStep: React.FC<ThemeSelectionStepProps> = ({
};

const handleBack = () => {
dispatch(setIsEditing(true));
dispatch(previousStep());
};
if (localStorage.getItem('themeChosen')) {

if (localStorage.getItem('themeChosen') === 'true' && !isEditing) {
return null;
}

const progressPercent = Math.round(
((currentStepDisplayIndex + 1) / totalSteps) * 100,
);
const progressPercent = Math.round(((displayIndex + 1) / totalSteps) * 100);

return (
<>
<Card className="flex max-h-full w-1/2 flex-col border p-4">
<CardHeader className="p-3">
<div className="text-muted-foreground mb-1 flex justify-between text-xs">
<span>
Step {currentStepDisplayIndex + 1} of {totalSteps}
Step {displayIndex + 1} of {totalSteps}
</span>
<span>{progressPercent}%</span>
</div>

<div className="bg-muted mb-4 h-2 w-full rounded-full">
<div
className="bg-primary h-full rounded-full transition-all duration-300"
Expand All @@ -80,6 +94,7 @@ export const ThemeSelectionStep: React.FC<ThemeSelectionStepProps> = ({
Choose your preferred appearance
</CardDescription>
</CardHeader>

<CardContent className="flex-1 space-y-6 overflow-y-auto p-1 px-2">
<RadioGroup
value={theme}
Expand Down Expand Up @@ -137,6 +152,7 @@ export const ThemeSelectionStep: React.FC<ThemeSelectionStepProps> = ({
</Button>
</CardFooter>
</Card>

<AppFeatures />
</>
);
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/features/onboardingSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface OnboardingState {
stepStatus: boolean[];
avatar: string | null;
name: string;
isEditing: boolean;
}

const initialState: OnboardingState = {
Expand All @@ -17,6 +18,7 @@ const initialState: OnboardingState = {
stepStatus: STEP_NAMES.map(() => false),
avatar: localStorage.getItem('avatar'),
name: localStorage.getItem('name') || '',
isEditing: false,
};
const onboardingSlice = createSlice({
name: 'onboarding',
Expand All @@ -28,6 +30,9 @@ const onboardingSlice = createSlice({
setName(state, action: PayloadAction<string>) {
state.name = action.payload;
},
setIsEditing(state, action: PayloadAction<boolean>) {
state.isEditing = action.payload;
},
markCompleted(state, action: PayloadAction<number>) {
const stepIndex = action.payload;
if (stepIndex >= 0 && stepIndex < state.stepStatus.length) {
Expand All @@ -51,7 +56,7 @@ const onboardingSlice = createSlice({
},
});

export const { setAvatar, setName, markCompleted, previousStep } =
export const { setAvatar, setName, setIsEditing, markCompleted, previousStep } =
onboardingSlice.actions;

export default onboardingSlice.reducer;