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
1 change: 1 addition & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/messages ./messages

USER nextjs

Expand Down
64 changes: 33 additions & 31 deletions frontend/app/dashboard/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Progress } from '@/components/ui/progress';
import { useAnalytics } from '@/lib/hooks/use-analytics';
import Image from 'next/image';
import Link from 'next/link';
import { useTranslations } from 'next-intl';

function StatCard({
title,
Expand Down Expand Up @@ -195,14 +196,15 @@ function AcceptanceTrendChart({ data }: { data: { period: string; rate: number;
}

export default function AnalyticsPage() {
const t = useTranslations('analytics');
const { data, isLoading, isError } = useAnalytics(60);

if (isLoading) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground">Your wardrobe insights and statistics</p>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">{t('subtitle')}</p>
</div>
<LoadingSkeleton />
</div>
Expand All @@ -212,7 +214,7 @@ export default function AnalyticsPage() {
if (isError || !data) {
return (
<div className="text-center py-8 text-red-500">
Failed to load analytics. Please try again.
{t('loadError')}
</div>
);
}
Expand All @@ -222,35 +224,35 @@ export default function AnalyticsPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground">Your wardrobe insights and statistics</p>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">{t('subtitle')}</p>
</div>

{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Total Items"
title={t('stats.totalItems.title')}
value={wardrobe.total_items}
description={`${wardrobe.items_by_status.ready} ready to wear`}
description={t('stats.totalItems.description', { count: wardrobe.items_by_status.ready })}
icon={Shirt}
/>
<StatCard
title="Outfits Generated"
title={t('stats.outfitsGenerated.title')}
value={wardrobe.total_outfits}
description={`${wardrobe.outfits_this_week} this week`}
description={t('stats.outfitsGenerated.description', { count: wardrobe.outfits_this_week })}
icon={Sparkles}
/>
<StatCard
title="Acceptance Rate"
title={t('stats.acceptanceRate.title')}
value={wardrobe.acceptance_rate ? `${wardrobe.acceptance_rate}%` : '-'}
description={wardrobe.acceptance_rate ? 'of suggestions accepted' : 'No data yet'}
description={wardrobe.acceptance_rate ? t('stats.acceptanceRate.description') : t('stats.totalWears.noData')}
icon={TrendingUp}
trend={wardrobe.acceptance_rate && wardrobe.acceptance_rate > 50 ? 'up' : undefined}
/>
<StatCard
title="Total Wears"
title={t('stats.totalWears.title')}
value={wardrobe.total_wears}
description={wardrobe.average_rating ? `Avg rating: ${wardrobe.average_rating}/5` : 'Track your outfits'}
description={wardrobe.average_rating ? t('stats.avgRating', { rating: wardrobe.average_rating }) : t('stats.totalWears.description')}
icon={Activity}
/>
</div>
Expand All @@ -261,7 +263,7 @@ export default function AnalyticsPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lightbulb className="h-5 w-5" />
Insights
{t('insights.title')}
</CardTitle>
</CardHeader>
<CardContent>
Expand All @@ -283,13 +285,13 @@ export default function AnalyticsPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChart className="h-5 w-5" />
Color Distribution
{t('insights.colorDistribution.title')}
</CardTitle>
<CardDescription>Most common colors in your wardrobe</CardDescription>
<CardDescription>{t('insights.colorDistribution.description')}</CardDescription>
</CardHeader>
<CardContent>
{color_distribution.length === 0 ? (
<p className="text-muted-foreground text-sm">No color data yet</p>
<p className="text-muted-foreground text-sm">{t('insights.colorDistribution.noData')}</p>
) : (
<div className="space-y-3">
{color_distribution.slice(0, 8).map((color) => (
Expand All @@ -305,13 +307,13 @@ export default function AnalyticsPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart className="h-5 w-5" />
Item Types
{t('insights.itemTypes.title')}
</CardTitle>
<CardDescription>Breakdown by clothing type</CardDescription>
<CardDescription>{t('insights.itemTypes.description')}</CardDescription>
</CardHeader>
<CardContent>
{type_distribution.length === 0 ? (
<p className="text-muted-foreground text-sm">No items yet</p>
<p className="text-muted-foreground text-sm">{t('insights.itemTypes.noData')}</p>
) : (
<div className="space-y-3">
{type_distribution.map((type) => (
Expand All @@ -335,12 +337,12 @@ export default function AnalyticsPage() {
{/* Most Worn */}
<Card>
<CardHeader>
<CardTitle>Most Worn</CardTitle>
<CardDescription>Your favorites</CardDescription>
<CardTitle>{t('insights.mostWorn.title')}</CardTitle>
<CardDescription>{t('insights.mostWorn.description')}</CardDescription>
</CardHeader>
<CardContent>
{most_worn.length === 0 ? (
<p className="text-muted-foreground text-sm">Start tracking your outfits!</p>
<p className="text-muted-foreground text-sm">{t('insights.mostWorn.noData')}</p>
) : (
<div className="space-y-1">
{most_worn.map((item) => (
Expand All @@ -354,12 +356,12 @@ export default function AnalyticsPage() {
{/* Least Worn */}
<Card>
<CardHeader>
<CardTitle>Least Worn</CardTitle>
<CardDescription>Consider wearing these</CardDescription>
<CardTitle>{t('insights.leastWorn.title')}</CardTitle>
<CardDescription>{t('insights.leastWorn.description')}</CardDescription>
</CardHeader>
<CardContent>
{least_worn.length === 0 ? (
<p className="text-muted-foreground text-sm">Keep tracking!</p>
<p className="text-muted-foreground text-sm">{t('insights.leastWorn.noData')}</p>
) : (
<div className="space-y-1">
{least_worn.map((item) => (
Expand All @@ -373,12 +375,12 @@ export default function AnalyticsPage() {
{/* Never Worn */}
<Card>
<CardHeader>
<CardTitle>Never Worn</CardTitle>
<CardDescription>Time to try these?</CardDescription>
<CardTitle>{t('insights.neverWorn.title')}</CardTitle>
<CardDescription>{t('insights.neverWorn.description')}</CardDescription>
</CardHeader>
<CardContent>
{never_worn.length === 0 ? (
<p className="text-muted-foreground text-sm">All items have been worn!</p>
<p className="text-muted-foreground text-sm">{t('insights.neverWorn.noData')}</p>
) : (
<div className="space-y-1">
{never_worn.map((item) => (
Expand All @@ -394,8 +396,8 @@ export default function AnalyticsPage() {
{acceptance_trend.length > 0 && acceptance_trend.some((t) => t.total > 0) && (
<Card>
<CardHeader>
<CardTitle>Acceptance Rate Trend</CardTitle>
<CardDescription>How you&apos;ve responded to suggestions over time</CardDescription>
<CardTitle>{t('insights.acceptanceTrend.title')}</CardTitle>
<CardDescription>{t('insights.acceptanceTrend.description')}</CardDescription>
</CardHeader>
<CardContent>
<AcceptanceTrendChart data={acceptance_trend} />
Expand Down
56 changes: 30 additions & 26 deletions frontend/app/dashboard/family/feed/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { FamilyRatingForm, FamilyRatingsDisplay } from '@/components/family-rati
import { OutfitPreviewDialog } from '@/components/outfit-preview-dialog';
import Image from 'next/image';
import Link from 'next/link';
import { useTranslations } from 'next-intl';

function getInitials(name: string) {
return name
Expand All @@ -35,25 +36,26 @@ function getInitials(name: string) {
}

function SourceBadge({ source }: { source: OutfitSource }) {
const t = useTranslations('familyFeed');
const config: Record<OutfitSource, { icon: typeof Calendar; label: string; className: string }> = {
scheduled: {
icon: Calendar,
label: 'Scheduled',
label: t('sourceBadges.scheduled'),
className: 'bg-primary/10 text-primary border-primary/20',
},
on_demand: {
icon: Zap,
label: 'On Demand',
label: t('sourceBadges.onDemand'),
className: 'bg-orange-500/10 text-orange-600 border-orange-500/20',
},
manual: {
icon: Edit3,
label: 'Manual',
label: t('sourceBadges.manual'),
className: 'bg-purple-500/10 text-purple-600 border-purple-500/20',
},
pairing: {
icon: Zap,
label: 'Pairing',
label: t('sourceBadges.pairing'),
className: 'bg-violet-500/10 text-violet-600 border-violet-500/20',
},
};
Expand All @@ -79,6 +81,7 @@ function FeedOutfitCard({
memberName: string;
onPreview: () => void;
}) {
const t = useTranslations('familyFeed');
const [showRatingForm, setShowRatingForm] = useState(false);
const myRating = outfit.family_ratings?.find((r) => r.user_id === currentMemberId);

Expand All @@ -98,7 +101,7 @@ function FeedOutfitCard({
month: 'short',
day: 'numeric',
year: 'numeric',
}) : 'Lookbook'}
}) : t('lookbook')}
</span>
</div>

Expand Down Expand Up @@ -152,7 +155,7 @@ function FeedOutfitCard({
))}
</div>
<span className="text-muted-foreground text-xs">
({outfit.family_rating_count} rating{outfit.family_rating_count !== 1 ? 's' : ''})
{t('ratingCount', { count: outfit.family_rating_count })}
</span>
</div>
)}
Expand Down Expand Up @@ -183,13 +186,13 @@ function FeedOutfitCard({
onClick={() => setShowRatingForm(true)}
>
<Star className="h-4 w-4 mr-2" />
Rate {memberName}&apos;s outfit
{t('rateOutfit', { member: memberName })}
</Button>
)
) : (
<div className="flex items-center justify-between pt-2 border-t">
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Your rating:</span>
<span className="text-muted-foreground">{t('yourRating')}</span>
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<Star
Expand All @@ -214,7 +217,7 @@ function FeedOutfitCard({
className="text-xs"
onClick={() => setShowRatingForm(!showRatingForm)}
>
Edit
{t('edit')}
</Button>
</div>
)}
Expand All @@ -235,27 +238,28 @@ function FeedOutfitCard({
}

function NoFamilyState() {
const t = useTranslations('familyFeed');
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">Family Feed</h1>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
Browse and rate your family members&apos; outfits
{t('subtitle')}
</p>
</div>

<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
<div className="rounded-full bg-muted p-6 mb-4">
<Users className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">Join a family first</h3>
<h3 className="text-lg font-semibold mb-2">{t('noFamily.title')}</h3>
<p className="text-muted-foreground mb-6 max-w-sm">
Create or join a family to browse and rate each other&apos;s outfits.
{t('noFamily.description')}
</p>
<Button asChild>
<Link href="/dashboard/family">
<Users className="mr-2 h-4 w-4" />
Set Up Family
{t('noFamily.setUpFamily')}
</Link>
</Button>
</div>
Expand All @@ -264,6 +268,7 @@ function NoFamilyState() {
}

function FeedContent() {
const t = useTranslations('familyFeed');
const { data: session } = useSession();
const { data: family, isLoading: familyLoading } = useFamily();
const currentEmail = session?.user?.email;
Expand Down Expand Up @@ -296,15 +301,15 @@ function FeedContent() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Family Feed</h1>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
Browse and rate your family members&apos; outfits
{t('subtitle')}
</p>
</div>
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard/family">
<Settings className="h-4 w-4 mr-2" />
Manage Family
{t('noMembers.manageFamily')}
</Link>
</Button>
</div>
Expand All @@ -313,13 +318,13 @@ function FeedContent() {
<div className="rounded-full bg-muted p-6 mb-4">
<Users className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">No other members yet</h3>
<h3 className="text-lg font-semibold mb-2">{t('noMembers.title')}</h3>
<p className="text-muted-foreground mb-6 max-w-sm">
Invite family members to start browsing and rating each other&apos;s outfits.
{t('noMembers.description')}
</p>
<Button asChild>
<Link href="/dashboard/family">
Invite Members
{t('noMembers.inviteMembers')}
</Link>
</Button>
</div>
Expand All @@ -332,15 +337,15 @@ function FeedContent() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Family Feed</h1>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-muted-foreground">
Browse and rate your family members&apos; outfits
{t('subtitle')}
</p>
</div>
<Button variant="outline" size="sm" asChild>
<Link href="/dashboard/family">
<Settings className="h-4 w-4 mr-2" />
Manage Family
{t('noMembers.manageFamily')}
</Link>
</Button>
</div>
Expand Down Expand Up @@ -394,10 +399,9 @@ function FeedContent() {
) : !data || data.outfits.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
<Shirt className="h-10 w-10 text-muted-foreground mb-3" />
<h3 className="text-base font-semibold mb-1">No outfits yet</h3>
<h3 className="text-base font-semibold mb-1">{t('noOutfits.title')}</h3>
<p className="text-sm text-muted-foreground max-w-xs">
{selectedMemberInfo?.display_name ?? 'This member'} hasn&apos;t received any outfit recommendations yet.
Check back later!
{t('noOutfits.description', { member: selectedMemberInfo?.display_name ?? 'This member' })}
</p>
</div>
) : (
Expand Down
Loading
Loading