Features implemented: - F1: Email digest notifications with cron endpoint and per-user frequency - F2: Jury availability windows and workload preferences in smart assignment - F3: Round templates with save-from-round and CRUD management - F4: Side-by-side project comparison view for jury members - F5: Real-time voting dashboard with Server-Sent Events (SSE) - F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations - F7: File versioning, inline preview, bulk download with presigned URLs - F8: Mentor dashboard: milestones, private notes, activity tracking - F9: Communication hub with broadcasts, templates, and recipient targeting - F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export - F11: Applicant draft saving with magic link resume and cron cleanup - F12: Webhook integration layer with HMAC signing, retry, and delivery logs - F13: Peer review discussions with anonymized scores and threaded comments - F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention - F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program New routers: roundTemplate, message, webhook (registered in _app.ts) New services: email-digest, webhook-dispatcher New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download All features are admin-configurable via SystemSettings or per-model settingsJson fields. Docker build verified successfully. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
'use client'
|
|
|
|
import { useTransition } from 'react'
|
|
import { useLocale } from 'next-intl'
|
|
import { useRouter } from 'next/navigation'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Globe, Check } from 'lucide-react'
|
|
|
|
const LANGUAGES = [
|
|
{ code: 'en', label: 'English', flag: 'EN' },
|
|
{ code: 'fr', label: 'Fran\u00e7ais', flag: 'FR' },
|
|
] as const
|
|
|
|
type LanguageCode = (typeof LANGUAGES)[number]['code']
|
|
|
|
export function LanguageSwitcher() {
|
|
const locale = useLocale() as LanguageCode
|
|
const router = useRouter()
|
|
const [isPending, startTransition] = useTransition()
|
|
|
|
const currentLang = LANGUAGES.find((l) => l.code === locale) ?? LANGUAGES[0]
|
|
|
|
const switchLanguage = (code: LanguageCode) => {
|
|
// Set cookie with 1 year expiry
|
|
document.cookie = `locale=${code};path=/;max-age=${365 * 24 * 60 * 60};samesite=lax`
|
|
// Refresh to re-run server components with new locale
|
|
startTransition(() => {
|
|
router.refresh()
|
|
})
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="gap-2" disabled={isPending}>
|
|
<Globe className="h-4 w-4" />
|
|
<span className="font-medium">{currentLang.flag}</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{LANGUAGES.map((lang) => (
|
|
<DropdownMenuItem
|
|
key={lang.code}
|
|
onClick={() => switchLanguage(lang.code)}
|
|
className="gap-2"
|
|
>
|
|
<span className="font-medium w-6">{lang.flag}</span>
|
|
<span>{lang.label}</span>
|
|
{locale === lang.code && <Check className="ml-auto h-4 w-4" />}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|