Skip to content
Merged
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
8 changes: 8 additions & 0 deletions src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HelpButton } from '@/components/HelpButton';
import { calculateWeakQuestionIds } from '@/lib/weakQuestions';
import { filterByTestType } from '@/lib/testTypeUtils';
import { SkipLink } from '@/components/SkipLink';
import { useCommunityPromoToast } from '@/hooks/useCommunityPromoToast';

interface AppLayoutProps {
children: ReactNode;
Expand Down Expand Up @@ -79,6 +80,13 @@ export function AppLayout({ children, currentView, onViewChange, selectedTest, o
(b) => b.display_name
);

// Show community promo toast for eligible users
useCommunityPromoToast({
userCreatedAt: user?.created_at,
forumUsername: profile?.forum_username,
isAuthenticated: !!user,
});

const handleSignOut = async () => {
// Navigate first to ensure we redirect before state changes trigger re-renders
navigate('/auth');
Expand Down
16 changes: 8 additions & 8 deletions src/components/DashboardSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,20 +390,20 @@ describe('DashboardSidebar', () => {
});
});

describe('Forum Link', () => {
it('displays Forum link', () => {
describe('Community Link', () => {
it('displays Community link', () => {
render(<DashboardSidebar {...defaultProps} />, { wrapper: createWrapper() });

expect(screen.getByText('Forum')).toBeInTheDocument();
expect(screen.getByText('Community')).toBeInTheDocument();
});

it('has correct href for Forum link', () => {
it('has correct href for Community link', () => {
render(<DashboardSidebar {...defaultProps} />, { wrapper: createWrapper() });

const forumLink = screen.getByText('Forum').closest('a');
expect(forumLink).toHaveAttribute('href', 'https://forum.openhamprep.com/auth/oidc');
expect(forumLink).toHaveAttribute('target', '_blank');
expect(forumLink).toHaveAttribute('rel', 'noopener noreferrer');
const communityLink = screen.getByText('Community').closest('a');
expect(communityLink).toHaveAttribute('href', 'https://forum.openhamprep.com/auth/oidc');
expect(communityLink).toHaveAttribute('target', '_blank');
expect(communityLink).toHaveAttribute('rel', 'noopener noreferrer');
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/components/DashboardSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function DashboardSidebar({
{ id: 'glossary', label: 'Glossary', icon: BookText },
{ id: 'tools', label: 'Tools', icon: Wrench },
{ id: 'find-test-site', label: 'Find Test Site', icon: MapPin },
{ id: 'forum', label: 'Forum', icon: Users, external: 'https://forum.openhamprep.com/auth/oidc' },
{ id: 'forum', label: 'Community', icon: Users, external: 'https://forum.openhamprep.com/auth/oidc' },
];

const handleNavClick = (view: View, disabled?: boolean) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProfileModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('ProfileModal', () => {
await user.click(accountMenuItem!);

expect(screen.getByText('Display Name')).toBeInTheDocument();
expect(screen.getByText('Forum Username')).toBeInTheDocument();
expect(screen.getByText('Community Username')).toBeInTheDocument();
expect(screen.getByText('Email Address')).toBeInTheDocument();
expect(screen.getByText('Password')).toBeInTheDocument();
});
Expand Down
4 changes: 2 additions & 2 deletions src/components/ProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ export function ProfileModal({

<EditableField
icon={MessageSquare}
label="Forum Username"
label="Community Username"
value={forumUsername}
displayValue={userInfo.forumUsername || ""}
onChange={setForumUsername}
Expand All @@ -489,7 +489,7 @@ export function ProfileModal({
}}
isSaving={isUpdatingForumUsername}
placeholder="Enter username"
helperText="Visible on the Open Ham Prep forum"
helperText="Visible on the Open Ham Prep community"
/>

<div className="space-y-2">
Expand Down
6 changes: 3 additions & 3 deletions src/components/sidebar/SidebarNavItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('SidebarNavItem', () => {
describe('External Links', () => {
const externalItem = {
id: 'forum',
label: 'Forum',
label: 'Community',
icon: BarChart3,
external: 'https://forum.example.com',
};
Expand Down Expand Up @@ -161,7 +161,7 @@ describe('SidebarNavItem', () => {
onClick={vi.fn()}
/>
);
expect(screen.queryByText('Forum')).not.toBeInTheDocument();
expect(screen.queryByText('Community')).not.toBeInTheDocument();
// Should only have one SVG (the item icon) when collapsed
const link = screen.getByRole('link');
const svgs = link.querySelectorAll('svg');
Expand All @@ -177,7 +177,7 @@ describe('SidebarNavItem', () => {
onClick={vi.fn()}
/>
);
expect(screen.getByText('Forum')).toBeInTheDocument();
expect(screen.getByText('Community')).toBeInTheDocument();
});
});

Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;

const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl border p-4 shadow-lg transition-all duration-300 ease-out data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-5 data-[state=open]:duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-5 data-[state=closed]:duration-300 data-[swipe=end]:animate-out",
{
variants: {
variant: {
default: "border bg-background text-foreground",
default: "border-border bg-card text-card-foreground",
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
Expand Down Expand Up @@ -67,7 +67,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 hover:text-foreground group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:outline-none focus:ring-2 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
"shrink-0 rounded-md p-1 text-muted-foreground transition-colors hover:text-foreground hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400",
className,
)}
toast-close=""
Expand Down
14 changes: 9 additions & 5 deletions src/components/ui/toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ export function Toaster() {
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
<div className="flex flex-col gap-3 w-full">
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription className="mt-1">{description}</ToastDescription>}
</div>
<ToastClose />
</div>
{action && <div className="flex justify-end">{action}</div>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
Expand Down
3 changes: 3 additions & 0 deletions src/hooks/use-toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ function toast({ ...props }: Toast) {
id,
open: true,
onOpenChange: (open) => {
// Call custom onOpenChange first to allow side effects (e.g., localStorage writes)
// before the toast is dismissed from the UI
props.onOpenChange?.(open);
if (!open) dismiss();
},
},
Expand Down
Loading
Loading