Skip to content
Open
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: 42 additions & 0 deletions frontend/src/components/Dialog/LogoutDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
} from '@/components/ui/alert-dialog';

interface LogoutDialogProps {
open: boolean;
onCancel: () => void;
onConfirm: () => void;
}

export function LogoutDialog({ open, onCancel, onConfirm }: LogoutDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onCancel}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Log out</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to log out? You’ll need to sign in again to
access your account.
</AlertDialogDescription>
</AlertDialogHeader>

<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onConfirm}
>
Logout
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
36 changes: 20 additions & 16 deletions frontend/src/components/Navigation/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ import { convertFileSrc } from '@tauri-apps/api/core';
import { FaceSearchDialog } from '@/components/Dialog/FaceSearchDialog';

export function Navbar() {
const dispatch = useDispatch();

const userName = useSelector(selectName);
const userAvatar = useSelector(selectAvatar);

const searchState = useSelector((state: any) => state.search);
const isSearchActive = searchState.active;
const queryImage = searchState.queryImage;

const dispatch = useDispatch();
/* ---------------- Avatar Normalization (Tauri + Logout Safe) ---------------- */
const displayName = userName || 'Guest';

return (
<div className="sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b pr-4 backdrop-blur">
<div className="bg-background sticky top-0 z-40 flex h-14 w-full items-center justify-between border-b pr-4 backdrop-blur">
{/* Logo */}
<div className="flex w-[256px] items-center justify-center">
<a href="/" className="flex items-center space-x-2">
Expand All @@ -28,13 +32,13 @@ export function Navbar() {

{/* Search Bar */}
<div className="mx-auto flex max-w-md flex-1 justify-center px-4">
<div className="dark:bg-muted/50 flex w-full items-center gap-1 rounded-md bg-neutral-100 px-1 py-1">
<div className="bg-muted flex w-full items-center gap-1 rounded-md px-1 py-1">
{/* Query Image */}
{queryImage && (
<div className="relative mr-2 ml-2">
<img
src={
queryImage?.startsWith('data:')
queryImage.startsWith('data:')
? queryImage
: convertFileSrc(queryImage)
}
Expand All @@ -44,30 +48,27 @@ export function Navbar() {
{isSearchActive && (
<button
onClick={() => dispatch(clearSearch())}
className="absolute -top-1 -right-1 flex h-3 w-3 items-center justify-center rounded-full bg-red-600 text-[10px] leading-none text-white"
title="Close"
aria-label="Close"
className="absolute -top-1 -right-1 flex h-3 w-3 items-center justify-center rounded-full bg-red-600 text-[10px] text-white"
aria-label="Clear search"
>
</button>
)}
</div>
)}

{/* Input */}
{/* Search Input */}
<Input
type="search"
placeholder="Add to your search"
className="mr-2 flex-1 border-0 bg-neutral-200"
className="mr-2 flex-1 border-0 bg-transparent"
/>

{/* FaceSearch Dialog */}

{/* Face Search */}
<FaceSearchDialog />

<button
className="text-muted-foreground hover:bg-accent dark:hover:bg-accent/50 hover:text-foreground mx-1 cursor-pointer rounded-sm p-2"
title="Search"
className="text-muted-foreground hover:bg-accent hover:text-foreground mx-1 rounded-sm p-2 transition"
aria-label="Search"
>
<Search className="h-4 w-4" />
Expand All @@ -78,15 +79,18 @@ export function Navbar() {
{/* Right Side */}
<div className="flex items-center space-x-4">
<ThemeSelector />

<div className="flex items-center space-x-2">
<span className="hidden text-sm sm:inline-block">
Welcome <span className="text-muted-foreground">{userName}</span>
Welcome,&nbsp;
<span className="text-muted-foreground">{displayName}</span>
</span>
<a href="/settings" className="p-2">

<a href="/profile" className="p-2">
<img
src={userAvatar || '/photo1.png'}
className="hover:ring-primary/50 h-8 w-8 cursor-pointer rounded-full transition-all hover:ring-2"
alt="User avatar"
className="hover:ring-primary/50 h-8 w-8 cursor-pointer rounded-full transition hover:ring-2"
/>
</a>
</div>
Expand Down
155 changes: 155 additions & 0 deletions frontend/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';

import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

/* -------------------------------------------------------------------------- */
/* Root */
/* -------------------------------------------------------------------------- */

const AlertDialog = AlertDialogPrimitive.Root;

const AlertDialogTrigger = AlertDialogPrimitive.Trigger;

/* -------------------------------------------------------------------------- */
/* Portal */
/* -------------------------------------------------------------------------- */

const AlertDialogPortal = AlertDialogPrimitive.Portal;

const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/50 backdrop-blur-sm', className)}
{...props}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;

/* -------------------------------------------------------------------------- */
/* Content */
/* -------------------------------------------------------------------------- */

const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
'border-border bg-background fixed top-[50%] left-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-xl border p-6 shadow-xl',
className,
)}
{...props}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;

/* -------------------------------------------------------------------------- */
/* Header */
/* -------------------------------------------------------------------------- */

const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
);
AlertDialogHeader.displayName = 'AlertDialogHeader';

const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold', className)}
{...props}
/>
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;

const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
));
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName;

/* -------------------------------------------------------------------------- */
/* Footer */
/* -------------------------------------------------------------------------- */

const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className,
)}
{...props}
/>
);
AlertDialogFooter.displayName = 'AlertDialogFooter';

/* -------------------------------------------------------------------------- */
/* Actions */
/* -------------------------------------------------------------------------- */

const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants({ variant: 'destructive' }), className)}
{...props}
/>
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;

const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;

/* -------------------------------------------------------------------------- */
/* Exports */
/* -------------------------------------------------------------------------- */

export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
};
2 changes: 2 additions & 0 deletions frontend/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export const ROUTES = {
ALBUMS: 'albums',
MEMORIES: 'memories',
PERSON: 'person/:clusterId',
PROFILE: 'profile',
EDIT_PROFILE: 'profile/edit',
};
42 changes: 28 additions & 14 deletions frontend/src/features/onboardingSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,54 @@ const initialState: OnboardingState = {
avatar: localStorage.getItem('avatar'),
name: localStorage.getItem('name') || '',
};

const onboardingSlice = createSlice({
name: 'onboarding',
initialState,
reducers: {
setAvatar(state, action: PayloadAction<string>) {
state.avatar = action.payload;
localStorage.setItem('avatar', action.payload);
},

setName(state, action: PayloadAction<string>) {
state.name = action.payload;
localStorage.setItem('name', action.payload);
},

/** ✅ LOGOUT / RESET */
resetOnboarding() {
localStorage.removeItem('avatar');
localStorage.removeItem('name');
return initialState;
},
Comment on lines +36 to 41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

resetOnboarding returns stale initialState captured at module load time.

initialState is computed once when the module loads, reading from localStorage at that time. After clearing localStorage in resetOnboarding, returning this cached object still contains the old values that were in localStorage when the app started.

     resetOnboarding() {
       localStorage.removeItem('avatar');
       localStorage.removeItem('name');
-      return initialState;
+      return {
+        currentStepIndex: 0,
+        currentStepName: STEP_NAMES[0],
+        stepStatus: STEP_NAMES.map(() => false),
+        avatar: null,
+        name: '',
+      };
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** ✅ LOGOUT / RESET */
resetOnboarding() {
localStorage.removeItem('avatar');
localStorage.removeItem('name');
return initialState;
},
/** ✅ LOGOUT / RESET */
resetOnboarding() {
localStorage.removeItem('avatar');
localStorage.removeItem('name');
return {
currentStepIndex: 0,
currentStepName: STEP_NAMES[0],
stepStatus: STEP_NAMES.map(() => false),
avatar: null,
name: '',
};
},
🤖 Prompt for AI Agents
In frontend/src/features/onboardingSlice.ts around lines 36 to 41,
resetOnboarding currently returns the module-level initialState which was
computed once at module load (stale), so after removing localStorage keys you
must return a fresh state derived from current storage or default values instead
of the cached object; modify resetOnboarding to compute and return a new state
object (e.g., read localStorage for avatar/name or use empty/default values) or
call a helper getInitialState() that builds the state from current localStorage,
ensuring you do not return the original module-scoped initialState reference.


markCompleted(state, action: PayloadAction<number>) {
const stepIndex = action.payload;
if (stepIndex >= 0 && stepIndex < state.stepStatus.length) {
state.stepStatus[stepIndex] = true;
} else {
console.warn(
`Invalid step index: ${stepIndex}. Valid range: 0-${state.stepStatus.length - 1}`,
);
const index = action.payload;
if (index >= 0 && index < state.stepStatus.length) {
state.stepStatus[index] = true;
}
state.currentStepIndex = state.stepStatus.findIndex((status) => !status);
state.currentStepIndex = state.stepStatus.findIndex((s) => !s);
state.currentStepName = STEP_NAMES[state.currentStepIndex] || '';
},

previousStep(state) {
const lastCompletedIndex = state.stepStatus.lastIndexOf(true);
if (lastCompletedIndex !== -1) {
state.stepStatus[lastCompletedIndex] = false;
const lastCompleted = state.stepStatus.lastIndexOf(true);
if (lastCompleted !== -1) {
state.stepStatus[lastCompleted] = false;
}
state.currentStepIndex = state.stepStatus.findIndex((status) => !status);
state.currentStepIndex = state.stepStatus.findIndex((s) => !s);
state.currentStepName = STEP_NAMES[state.currentStepIndex] || '';
},
},
});

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

export default onboardingSlice.reducer;
Loading