Skip to content

Feat/shadcn themes #912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
03ff101
feat: add theme management features and UI components
umarhadi May 8, 2025
5686f9b
feat: implement default theme creation and navigation updates
umarhadi May 8, 2025
74205e6
feat: add theme application functionality to active connections
umarhadi May 8, 2025
59a479d
feat: integrate custom theme provider and enhance theme management wi…
umarhadi May 8, 2025
0486c23
feat: enhance theme application and management with connection target…
umarhadi May 8, 2025
f3fb37b
feat: enhance theme editor with font styling and expand custom theme …
umarhadi May 8, 2025
9a77278
feat: update theme settings to include foreground options for card, p…
umarhadi May 8, 2025
a363b4c
feat: add sidebar theme selector component and integrate it into the …
umarhadi May 8, 2025
10d0c2c
feat: add reset to default theme functionality in sidebar theme selec…
umarhadi May 8, 2025
85d4459
feat: implement preset theme creation functionality and enhance theme…
umarhadi May 8, 2025
290d07a
refactor: update styling in mail components to use new theme variable…
umarhadi May 8, 2025
3c76df9
feat: add new theme presets including Amber Minimal, Amethyst Haze, B…
umarhadi May 8, 2025
581539f
Merge remote-tracking branch 'upstream/staging' into feat/shadcn-themes
umarhadi May 8, 2025
a45dfd2
feat: migrate theme management to use tRPC for API interactions; remo…
umarhadi May 8, 2025
fb5fe6b
Merge remote-tracking branch 'upstream/staging' into feat/shadcn-themes
umarhadi May 9, 2025
747374f
refactor: update import statements
umarhadi May 9, 2025
115d7c1
refactor: use react-query
umarhadi May 9, 2025
71c687c
Merge branch 'staging' into feat/shadcn-themes
umarhadi May 9, 2025
0306991
chore: update lockfile
umarhadi May 9, 2025
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
2 changes: 1 addition & 1 deletion apps/mail/app/(routes)/mail/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function MailLayout({ children }: { children: React.ReactNode })
return (
<HotkeyProviderWrapper>
<AppSidebar />
<div className="bg-lightBackground dark:bg-darkBackground w-full">{children}</div>
<div className="bg-background w-full">{children}</div>
<OnboardingWrapper />
</HotkeyProviderWrapper>
);
Expand Down
31 changes: 29 additions & 2 deletions apps/mail/app/(routes)/settings/connections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ import { emailProviders } from '@/lib/constants';
import { Button } from '@/components/ui/button';
import { useSession } from '@/lib/auth-client';
import { useTranslations } from 'next-intl';
import { Trash, Plus } from 'lucide-react';
import { useState } from 'react';
import { Trash, Plus, Palette } from 'lucide-react';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { toast } from 'sonner';
import { useUserThemes, useThemeActions } from '@/hooks/use-themes';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import Link from 'next/link';
import { useRouter } from 'next/navigation';

export default function ConnectionsPage() {
const { data, isLoading, refetch: refetchConnections } = useConnections();
Expand Down Expand Up @@ -132,6 +136,28 @@ export default function ConnectionsPage() {
</div>
</div>
</div>
<div className="flex items-center gap-1 ml-4 shrink-0">
<Select
value={getConnectionCurrentThemeId(connection.id)}
onValueChange={(newThemeId) => handleThemeChange(newThemeId, connection.id)}
>
<SelectTrigger className="w-[180px] h-9" aria-label={`Theme for ${connection.name}`}>
<Palette className="h-4 w-4 mr-2 opacity-50" />
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="system-default">System Default</SelectItem>
{userThemes && userThemes.map(theme => (
<SelectItem key={theme.id} value={theme.id}>{theme.name}</SelectItem>
))}
<SelectItem value="manage-themes">
<Link href="/settings/themes/theme-management" className="flex items-center">
<Plus className="h-4 w-4 mr-2" /> Manage / Create
</Link>
</SelectItem>
</SelectContent>
</Select>

<Dialog>
<DialogTrigger asChild>
<Button
Expand Down Expand Up @@ -163,6 +189,7 @@ export default function ConnectionsPage() {
</div>
</DialogContent>
</Dialog>
</div>
</div>
))}
</div>
Expand Down
175 changes: 175 additions & 0 deletions apps/mail/app/(routes)/settings/themes/marketplace/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client';

import { useEffect, useState } from 'react';
import { usePublicThemes, useThemeActions } from '@/hooks/use-themes';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ThemePreview } from '@/components/theme/theme-editor';
import { Separator } from '@/components/ui/separator';
import { Copy, ArrowLeft } from 'lucide-react';
import { toast } from 'sonner';
import { EmptyPlaceholder } from '@/components/empty-placeholder';
import Link from 'next/link';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';

export default function ThemeMarketplacePage() {
const { themes, isLoading } = usePublicThemes();
const { copy } = useThemeActions();
const [searchQuery, setSearchQuery] = useState('');
const [filteredThemes, setFilteredThemes] = useState(themes);

useEffect(() => {
if (searchQuery.trim() === '') {
setFilteredThemes(themes);
} else {
setFilteredThemes(
themes.filter(theme =>
theme.name.toLowerCase().includes(searchQuery.toLowerCase())
)
);
}
}, [searchQuery, themes]);

const handleCopyTheme = async (id: string, name: string) => {
try {
const result = await copy(id);
if (result.success) {
toast.success(`Theme "${name}" copied to your collection`);
} else {
toast.error(result.error || 'Failed to copy theme');
}
} catch (error) {
console.error('Error copying theme:', error);
toast.error('Failed to copy theme');
}
};

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/settings/themes">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h2 className="text-2xl font-semibold">Theme Marketplace</h2>
</div>
</div>

<div className="flex items-center justify-between">
<p className="text-muted-foreground">
Browse and copy themes created by the community
</p>
<div className="w-1/3">
<Input
placeholder="Search themes..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>

<Tabs defaultValue="all">
<TabsList>
<TabsTrigger value="all">All Themes</TabsTrigger>
<TabsTrigger value="light">Light Themes</TabsTrigger>
<TabsTrigger value="dark">Dark Themes</TabsTrigger>
<TabsTrigger value="colorful">Colorful</TabsTrigger>
</TabsList>

<TabsContent value="all" className="mt-6">
{isLoading ? (
<div className="text-center p-12">Loading themes...</div>
) : filteredThemes.length === 0 ? (
<EmptyPlaceholder>
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted">
<Copy className="h-10 w-10 text-muted-foreground" />
</div>
<h3 className="mt-4 text-lg font-semibold">No themes found</h3>
<p className="mb-4 mt-2 text-center text-sm text-muted-foreground">
{searchQuery
? "No themes match your search. Try a different query."
: "There are no public themes available yet."}
</p>
</EmptyPlaceholder>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredThemes.map((theme) => (
<Card key={theme.id} className="overflow-hidden border hover:shadow-md transition-shadow">
<div
className="h-40 cursor-pointer relative"
style={{
backgroundColor: theme.settings.colors.background,
background: theme.settings.background.type === 'color'
? theme.settings.background.value
: theme.settings.background.type === 'gradient'
? theme.settings.background.value
: `url(${theme.settings.background.value})`,
backgroundSize: 'cover',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '1rem',
}}
>
<div
className="rounded-lg p-3"
style={{
backgroundColor: theme.settings.colors.muted,
color: theme.settings.colors.foreground,
border: `1px solid ${theme.settings.colors.border}`,
fontFamily: theme.settings.fonts.family,
boxShadow: `0 0 ${theme.settings.shadows.intensity}px ${theme.settings.shadows.color}`,
borderRadius: `${theme.settings.cornerRadius}px`,
}}
>
{theme.name}
</div>
</div>
<div className="p-4">
<div className="flex items-center justify-between">
<h4 className="font-semibold">{theme.name}</h4>
<Button
size="sm"
variant="ghost"
className="px-2"
onClick={() => handleCopyTheme(theme.id, theme.name)}
>
<Copy className="h-4 w-4 mr-1" /> Copy
</Button>
</div>
<div className="flex gap-2 mt-3">
{Object.entries(theme.settings.colors).slice(0, 5).map(([key, color]) => (
<div
key={key}
className="w-6 h-6 rounded-full border"
style={{ backgroundColor: color }}
title={`${key}: ${color}`}
/>
))}
</div>
</div>
</Card>
))}
</div>
)}
</TabsContent>

{/* Content for other tabs would filter the themes accordingly */}
<TabsContent value="light" className="mt-6">
{/* Similar to "all" but filtered for light themes */}
</TabsContent>

<TabsContent value="dark" className="mt-6">
{/* Similar to "all" but filtered for dark themes */}
</TabsContent>

<TabsContent value="colorful" className="mt-6">
{/* Similar to "all" but filtered for colorful themes */}
</TabsContent>
</Tabs>
</div>
);
}
64 changes: 64 additions & 0 deletions apps/mail/app/(routes)/settings/themes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import { Separator } from '@/components/ui/separator';
import { ThemeManagement } from './theme-management';
import { CustomThemeProvider } from '@/providers/custom-theme-provider';
import { useSession } from '@/lib/auth-client';
import { EmptyPlaceholder } from '@/components/empty-placeholder';
import { useEffect, useState } from 'react';

export default function ThemesPage() {
const { data: session } = useSession();
// Add a local loading state, default to true until session is checked.
const [isSessionLoading, setIsSessionLoading] = useState(true);

useEffect(() => {
// session can be null if not logged in, or an object if logged in.
// consider loading finished once useSession has had a chance to run.
setIsSessionLoading(false);
}, [session]);

if (isSessionLoading) {
return (
<div className="space-y-6 p-4">
<div>Loading session...</div>
</div>
);
}

if (!session?.user?.id) {
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Themes</h3>
<p className="text-sm text-muted-foreground">
Customize the appearance of your email client with personalized themes
</p>
</div>
<Separator />
<EmptyPlaceholder>
<div className="text-center">
<p>You need to be signed in to manage themes.</p>
</div>
</EmptyPlaceholder>
</div>
);
}

return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium">Themes</h3>
<p className="text-sm text-muted-foreground">
Customize the appearance of your email client with personalized themes
</p>
</div>
<Separator />
<CustomThemeProvider>
{/* ThemeManagement will now fetch its own data. Passing empty initialThemes */}
{/* and ThemeManagement will handle loading and empty states itself */}
<ThemeManagement initialThemes={[]} />
</CustomThemeProvider>
</div>
);
}
Loading