Skip to content

Commit b635d23

Browse files
feat(dashboard): Implement content language handling
1 parent 4db0d51 commit b635d23

File tree

7 files changed

+313
-115
lines changed

7 files changed

+313
-115
lines changed

packages/dashboard/src/app/app-providers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
1414
return (
1515
<I18nProvider>
1616
<QueryClientProvider client={queryClient}>
17-
<UserSettingsProvider>
17+
<UserSettingsProvider queryClient={queryClient}>
1818
<ThemeProvider defaultTheme="system">
1919
<AuthProvider>
2020
<ServerConfigProvider>
Lines changed: 168 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChevronsUpDown, Plus } from 'lucide-react';
1+
import { ChevronsUpDown, Languages, Plus } from 'lucide-react';
22

33
import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
44
import {
@@ -7,80 +7,191 @@ import {
77
DropdownMenuItem,
88
DropdownMenuLabel,
99
DropdownMenuSeparator,
10+
DropdownMenuSub,
11+
DropdownMenuSubContent,
12+
DropdownMenuSubTrigger,
1013
DropdownMenuTrigger,
1114
} from '@/vdb/components/ui/dropdown-menu.js';
1215
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/vdb/components/ui/sidebar.js';
1316
import { useChannel } from '@/vdb/hooks/use-channel.js';
17+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
18+
import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
19+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
1420
import { Trans } from '@/vdb/lib/trans.js';
1521
import { Link } from '@tanstack/react-router';
22+
import { useState } from 'react';
23+
import { ManageLanguagesDialog } from './manage-languages-dialog.js';
24+
25+
/**
26+
* Convert the channel code to initials.
27+
* Splits by punctuation like '-' and '_' and takes the first letter of each part
28+
* up to 3 parts.
29+
*
30+
* If no splits, takes the first 3 letters.
31+
*/
32+
function getChannelInitialsFromCode(code: string) {
33+
const parts = code.split(/[-_]/);
34+
if (parts.length > 1) {
35+
return parts
36+
.filter(part => part.length > 0)
37+
.slice(0, 3)
38+
.map(part => part[0])
39+
.join('');
40+
} else {
41+
return code.slice(0, 3);
42+
}
43+
}
1644

1745
export function ChannelSwitcher() {
1846
const { isMobile } = useSidebar();
1947
const { channels, activeChannel, selectedChannel, setSelectedChannel } = useChannel();
48+
const serverConfig = useServerConfig();
49+
const { formatLanguageName } = useLocalFormat();
50+
const {
51+
settings: { contentLanguage },
52+
setContentLanguage,
53+
} = useUserSettings();
54+
const [showManageLanguagesDialog, setShowManageLanguagesDialog] = useState(false);
2055

2156
// Use the selected channel if available, otherwise fall back to the active channel
2257
const displayChannel = selectedChannel || activeChannel;
2358

59+
// Get available languages from server config
60+
const availableLanguages = serverConfig?.availableLanguages || [];
61+
const hasMultipleLanguages = availableLanguages.length > 1;
62+
63+
// Reorder channels to put the currently selected one first
64+
const orderedChannels = displayChannel
65+
? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
66+
: channels;
67+
68+
console.log(displayChannel);
69+
2470
return (
25-
<SidebarMenu>
26-
<SidebarMenuItem>
27-
<DropdownMenu>
28-
<DropdownMenuTrigger asChild>
29-
<SidebarMenuButton
30-
size="lg"
31-
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
32-
>
33-
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
34-
<span className="truncate font-semibold text-xs">
35-
{displayChannel?.defaultCurrencyCode}
36-
</span>
37-
</div>
38-
<div className="grid flex-1 text-left text-sm leading-tight">
39-
<span className="truncate font-semibold">
40-
<ChannelCodeLabel code={displayChannel?.code} />
41-
</span>
42-
<span className="truncate text-xs">
43-
Default Language: {displayChannel?.defaultLanguageCode?.toUpperCase()}
44-
</span>
45-
</div>
46-
<ChevronsUpDown className="ml-auto" />
47-
</SidebarMenuButton>
48-
</DropdownMenuTrigger>
49-
<DropdownMenuContent
50-
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
51-
align="start"
52-
side={isMobile ? 'bottom' : 'right'}
53-
sideOffset={4}
54-
>
55-
<DropdownMenuLabel className="text-muted-foreground text-xs">
56-
<Trans>Channels</Trans>
57-
</DropdownMenuLabel>
58-
{channels.map((channel, index) => (
59-
<DropdownMenuItem
60-
key={channel.code}
61-
onClick={() => setSelectedChannel(channel.id)}
62-
className="gap-2 p-2"
71+
<>
72+
<SidebarMenu>
73+
<SidebarMenuItem>
74+
<DropdownMenu>
75+
<DropdownMenuTrigger asChild>
76+
<SidebarMenuButton
77+
size="lg"
78+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
6379
>
64-
<div className="flex size-8 items-center justify-center rounded border">
65-
<span className="truncate font-semibold text-xs">
66-
{channel.defaultCurrencyCode}
80+
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
81+
<span className="truncate font-semibold text-xs uppercase">
82+
{getChannelInitialsFromCode(displayChannel?.code || '')}
6783
</span>
6884
</div>
69-
<ChannelCodeLabel code={channel.code} />
70-
</DropdownMenuItem>
71-
))}
72-
<DropdownMenuSeparator />
73-
<DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
74-
<Link to={'/channels/new'}>
75-
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
76-
<Plus className="size-4" />
85+
<div className="grid flex-1 text-left text-sm leading-tight">
86+
<span className="truncate font-semibold">
87+
<ChannelCodeLabel code={displayChannel?.code} />
88+
</span>
89+
<span className="truncate text-xs">
90+
{hasMultipleLanguages ? (
91+
<span className="cursor-pointer hover:text-foreground">
92+
Language: {formatLanguageName(contentLanguage)}
93+
</span>
94+
) : (
95+
<span>Language: {formatLanguageName(contentLanguage)}</span>
96+
)}
97+
</span>
7798
</div>
78-
<div className="text-muted-foreground font-medium">Add channel</div>
79-
</Link>
80-
</DropdownMenuItem>
81-
</DropdownMenuContent>
82-
</DropdownMenu>
83-
</SidebarMenuItem>
84-
</SidebarMenu>
99+
<ChevronsUpDown className="ml-auto" />
100+
</SidebarMenuButton>
101+
</DropdownMenuTrigger>
102+
<DropdownMenuContent
103+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
104+
align="start"
105+
side={isMobile ? 'bottom' : 'right'}
106+
sideOffset={4}
107+
>
108+
<DropdownMenuLabel className="text-muted-foreground text-xs">
109+
<Trans>Channels</Trans>
110+
</DropdownMenuLabel>
111+
{orderedChannels.map((channel, index) => (
112+
<div key={channel.code}>
113+
<DropdownMenuItem
114+
onClick={() => setSelectedChannel(channel.id)}
115+
className="gap-2 p-2"
116+
>
117+
<div className="flex size-8 items-center justify-center rounded border">
118+
<span className="truncate font-semibold text-xs uppercase">
119+
{getChannelInitialsFromCode(channel.code)}
120+
</span>
121+
</div>
122+
<ChannelCodeLabel code={channel.code} />
123+
{channel.id === displayChannel?.id && (
124+
<span className="ml-auto text-xs text-muted-foreground">
125+
Current
126+
</span>
127+
)}
128+
</DropdownMenuItem>
129+
{/* Show language sub-menu for the current channel */}
130+
{channel.id === displayChannel?.id && (
131+
<DropdownMenuSub>
132+
<DropdownMenuSubTrigger className="gap-2 p-2 pl-4">
133+
<Languages className="w-4 h-4" />
134+
<div className="flex gap-1 ml-2">
135+
<span className="text-muted-foreground">Content: </span>
136+
{formatLanguageName(contentLanguage)}
137+
</div>
138+
</DropdownMenuSubTrigger>
139+
<DropdownMenuSubContent>
140+
{channel.availableLanguageCodes?.map(languageCode => (
141+
<DropdownMenuItem
142+
key={`${channel.code}-${languageCode}`}
143+
onClick={() => setContentLanguage(languageCode)}
144+
className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
145+
>
146+
<div className="flex w-6 h-5 items-center justify-center rounded border">
147+
<span className="truncate font-medium text-xs">
148+
{languageCode.toUpperCase()}
149+
</span>
150+
</div>
151+
<span>{formatLanguageName(languageCode)}</span>
152+
{contentLanguage === languageCode && (
153+
<span className="ml-auto text-xs text-muted-foreground">
154+
Active
155+
</span>
156+
)}
157+
</DropdownMenuItem>
158+
))}
159+
<DropdownMenuSeparator />
160+
<DropdownMenuItem
161+
onClick={() => setShowManageLanguagesDialog(true)}
162+
className="gap-2 p-2"
163+
>
164+
<Languages className="w-4 h-4" />
165+
<span>
166+
<Trans>Manage Languages</Trans>
167+
</span>
168+
</DropdownMenuItem>
169+
</DropdownMenuSubContent>
170+
</DropdownMenuSub>
171+
)}
172+
{/* Add separator after the current channel group */}
173+
{channel.id === displayChannel?.id &&
174+
index === 0 &&
175+
orderedChannels.length > 1 && <DropdownMenuSeparator />}
176+
</div>
177+
))}
178+
<DropdownMenuSeparator />
179+
<DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
180+
<Link to={'/channels/new'}>
181+
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
182+
<Plus className="size-4" />
183+
</div>
184+
<div className="text-muted-foreground font-medium">Add channel</div>
185+
</Link>
186+
</DropdownMenuItem>
187+
</DropdownMenuContent>
188+
</DropdownMenu>
189+
</SidebarMenuItem>
190+
</SidebarMenu>
191+
<ManageLanguagesDialog
192+
open={showManageLanguagesDialog}
193+
onClose={() => setShowManageLanguagesDialog(false)}
194+
/>
195+
</>
85196
);
86197
}

0 commit comments

Comments
 (0)