Skip to content

Commit e233d72

Browse files
committed
state
1 parent d75cb2e commit e233d72

File tree

8 files changed

+419
-172
lines changed

8 files changed

+419
-172
lines changed

apps/dashboard/app/(main)/websites/[id]/_components/tabs/settings-tab.tsx

Lines changed: 102 additions & 80 deletions
Large diffs are not rendered by default.

apps/dashboard/app/(main)/websites/[id]/_components/tabs/tracking-setup-tab.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
FileCodeIcon,
1212
WarningCircleIcon,
1313
} from '@phosphor-icons/react';
14+
import { useAtom } from 'jotai';
1415
import { useCallback, useEffect, useState } from 'react';
1516
import { codeToHtml } from 'shiki';
1617
import { toast } from 'sonner';
@@ -26,14 +27,16 @@ import { Label } from '@/components/ui/label';
2627
import { Switch } from '@/components/ui/switch';
2728
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
2829
import { trpc } from '@/lib/trpc';
30+
import {
31+
toggleTrackingOptionAtom,
32+
trackingOptionsAtom,
33+
} from '@/stores/jotai/filterAtoms';
2934
import {
3035
generateNpmCode,
3136
generateNpmComponentCode,
3237
generateScriptTag,
3338
} from '../utils/code-generators';
3439

35-
import { RECOMMENDED_DEFAULTS } from '../utils/tracking-defaults';
36-
import { toggleTrackingOption } from '../utils/tracking-helpers';
3740
import type { TrackingOptions, WebsiteDataTabProps } from '../utils/types';
3841

3942
const CodeBlock = ({
@@ -91,25 +94,29 @@ const CodeBlock = ({
9194
)}
9295
<div className="relative">
9396
<div
94-
className='overflow-hidden rounded-lg border border-sidebar-border bg-sidebar/40 p-6 text-sm leading-relaxed [&_pre]:!bg-transparent [&_code]:!bg-transparent [&_*]:!font-mono'
97+
className="[&_pre]:!bg-transparent [&_code]:!bg-transparent [&_*]:!font-mono overflow-hidden rounded-lg border border-sidebar-border bg-sidebar/40 p-6 text-sm leading-relaxed"
9598
// biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki generates safe HTML
9699
dangerouslySetInnerHTML={{ __html: highlightedCode }}
97-
style={{
98-
fontSize: '14px',
99-
lineHeight: '1.6',
100-
fontFamily: 'var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
100+
style={{
101+
fontSize: '14px',
102+
lineHeight: '1.6',
103+
fontFamily:
104+
'var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
101105
}}
102106
/>
103107
<Button
104-
className='absolute top-2 right-2 h-6 w-6 rounded hover:bg-background/50 border-0 shadow-none transition-colors duration-200'
108+
className="absolute top-2 right-2 h-6 w-6 rounded border-0 shadow-none transition-colors duration-200 hover:bg-background/50"
105109
onClick={onCopy}
106110
size="icon"
107111
variant="ghost"
108112
>
109113
{copied ? (
110114
<CheckIcon className="h-3.5 w-3.5 text-green-500" weight="bold" />
111115
) : (
112-
<ClipboardIcon className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" weight="regular" />
116+
<ClipboardIcon
117+
className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground"
118+
weight="regular"
119+
/>
113120
)}
114121
</Button>
115122
</div>
@@ -122,8 +129,7 @@ export function WebsiteTrackingSetupTab({ websiteId }: WebsiteDataTabProps) {
122129
const [installMethod, setInstallMethod] = useState<'script' | 'npm'>(
123130
'script'
124131
);
125-
const [trackingOptions, setTrackingOptions] =
126-
useState<TrackingOptions>(RECOMMENDED_DEFAULTS);
132+
const [trackingOptions] = useAtom(trackingOptionsAtom);
127133

128134
const trackingCode = generateScriptTag(websiteId, trackingOptions);
129135
const npmCode = generateNpmCode(websiteId, trackingOptions);
@@ -135,8 +141,9 @@ export function WebsiteTrackingSetupTab({ websiteId }: WebsiteDataTabProps) {
135141
setTimeout(() => setCopiedBlockId(null), 2000);
136142
};
137143

144+
const [, toggleTrackingOptionAction] = useAtom(toggleTrackingOptionAtom);
138145
const handleToggleOption = (option: keyof TrackingOptions) => {
139-
setTrackingOptions((prev) => toggleTrackingOption(prev, option));
146+
toggleTrackingOptionAction(option);
140147
};
141148

142149
const utils = trpc.useUtils();
@@ -173,23 +180,29 @@ export function WebsiteTrackingSetupTab({ websiteId }: WebsiteDataTabProps) {
173180
<div className="flex items-center justify-between p-4">
174181
<div className="flex items-center gap-3">
175182
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/15 dark:bg-primary/20">
176-
<WarningCircleIcon className="h-4 w-4 text-primary" weight="duotone" />
183+
<WarningCircleIcon
184+
className="h-4 w-4 text-primary"
185+
weight="duotone"
186+
/>
177187
</div>
178188
<div className="flex flex-col gap-0.5">
179189
<span className="font-medium text-sm">Tracking Not Setup</span>
180-
<span className="text-muted-foreground text-xs font-normal">
190+
<span className="font-normal text-muted-foreground text-xs">
181191
Install the tracking script to start collecting data
182192
</span>
183193
</div>
184194
</div>
185195
<Button
186196
aria-label="Refresh tracking status"
187-
className="h-7 w-7 rounded-md hover:bg-background/20 border-0 shadow-none hover:shadow-sm transition-all duration-200"
197+
className="h-7 w-7 rounded-md border-0 shadow-none transition-all duration-200 hover:bg-background/20 hover:shadow-sm"
188198
onClick={handleRefresh}
189199
size="icon"
190200
variant="ghost"
191201
>
192-
<ArrowClockwiseIcon className="h-3.5 w-3.5 text-primary/70 hover:text-primary" weight="fill" />
202+
<ArrowClockwiseIcon
203+
className="h-3.5 w-3.5 text-primary/70 hover:text-primary"
204+
weight="fill"
205+
/>
193206
</Button>
194207
</div>
195208
</Card>

apps/dashboard/app/(main)/websites/[id]/page.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,7 @@ function WebsiteDetailsPage() {
8585

8686
const { dateRange } = useDateFilters();
8787

88-
const {
89-
data,
90-
isLoading,
91-
isError,
92-
refetch: refetchWebsiteData,
93-
} = useWebsite(id as string);
88+
const { data, isLoading, isError } = useWebsite(id as string);
9489

9590
const { isTrackingSetup } = useTrackingSetup(id as string);
9691

@@ -120,7 +115,6 @@ function WebsiteDetailsPage() {
120115
websiteId: id as string,
121116
dateRange,
122117
websiteData: data,
123-
onWebsiteUpdated: refetchWebsiteData,
124118
};
125119

126120
const tabProps: FullTabProps = {
@@ -161,7 +155,6 @@ function WebsiteDetailsPage() {
161155
data,
162156
isRefreshing,
163157
setIsRefreshing,
164-
refetchWebsiteData,
165158
selectedFilters,
166159
addFilter,
167160
]

apps/dashboard/components/website-dialog.tsx

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ import { Input } from '@/components/ui/input';
2828
import type { CreateWebsiteData, Website } from '@/hooks/use-websites';
2929
import { useCreateWebsite, useUpdateWebsite } from '@/hooks/use-websites';
3030

31+
interface UpdateWebsiteInput {
32+
id: string;
33+
name: string;
34+
domain?: string;
35+
isPublic?: boolean;
36+
}
37+
3138
const domainRegex =
3239
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
3340
const wwwRegex = /^www\./;
@@ -78,6 +85,38 @@ export function WebsiteDialog({
7885
}
7986
}, [website, form]);
8087

88+
const getErrorMessage = (error: unknown, isEditingMode: boolean): string => {
89+
const defaultMessage = `Failed to ${isEditingMode ? 'update' : 'create'} website.`;
90+
91+
// Type guard for TRPC error
92+
const trpcError = error as {
93+
data?: { code?: string };
94+
message?: string;
95+
};
96+
97+
if (trpcError?.data?.code) {
98+
switch (trpcError.data.code) {
99+
case 'CONFLICT':
100+
return 'A website with this domain already exists.';
101+
case 'FORBIDDEN':
102+
return (
103+
trpcError.message ||
104+
'You do not have permission to perform this action.'
105+
);
106+
case 'UNAUTHORIZED':
107+
return 'You must be logged in to perform this action.';
108+
case 'BAD_REQUEST':
109+
return (
110+
trpcError.message || 'Invalid request. Please check your input.'
111+
);
112+
default:
113+
return trpcError.message || defaultMessage;
114+
}
115+
}
116+
117+
return trpcError?.message || defaultMessage;
118+
};
119+
81120
const handleSubmit = form.handleSubmit(async (formData) => {
82121
const submissionData: CreateWebsiteData = {
83122
name: formData.name,
@@ -87,10 +126,12 @@ export function WebsiteDialog({
87126

88127
try {
89128
if (isEditing) {
90-
const result = await updateWebsiteMutation.mutateAsync({
129+
const updateData: UpdateWebsiteInput = {
91130
id: website.id,
92131
name: formData.name,
93-
});
132+
domain: formData.domain,
133+
};
134+
const result = await updateWebsiteMutation.mutateAsync(updateData);
94135
if (onSave) {
95136
onSave(result);
96137
}
@@ -103,33 +144,8 @@ export function WebsiteDialog({
103144
toast.success('Website created successfully!');
104145
}
105146
onOpenChange(false);
106-
} catch (error: any) {
107-
let message = `Failed to ${isEditing ? 'update' : 'create'} website.`;
108-
109-
if (error?.data?.code) {
110-
switch (error.data.code) {
111-
case 'CONFLICT':
112-
message = 'A website with this domain already exists.';
113-
break;
114-
case 'FORBIDDEN':
115-
message =
116-
error.message ||
117-
'You do not have permission to perform this action.';
118-
break;
119-
case 'UNAUTHORIZED':
120-
message = 'You must be logged in to perform this action.';
121-
break;
122-
case 'BAD_REQUEST':
123-
message =
124-
error.message || 'Invalid request. Please check your input.';
125-
break;
126-
default:
127-
message = error.message || message;
128-
}
129-
} else if (error?.message) {
130-
message = error.message;
131-
}
132-
147+
} catch (error: unknown) {
148+
const message = getErrorMessage(error, isEditing);
133149
toast.error(message);
134150
}
135151
});

apps/dashboard/hooks/use-websites.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,35 @@ export function useUpdateWebsite() {
9595
});
9696
}
9797

98+
export function useTogglePublicWebsite() {
99+
const utils = trpc.useUtils();
100+
return trpc.websites.togglePublic.useMutation({
101+
onSuccess: (updatedWebsite) => {
102+
const getByIdKey = { id: updatedWebsite.id };
103+
const listKey = {
104+
organizationId: updatedWebsite.organizationId ?? undefined,
105+
};
106+
107+
utils.websites.listWithCharts.setData(listKey, (old) => {
108+
if (!old) {
109+
return old;
110+
}
111+
return {
112+
...old,
113+
websites: old.websites.map((website) =>
114+
website.id === updatedWebsite.id ? updatedWebsite : website
115+
),
116+
};
117+
});
118+
119+
utils.websites.getById.setData(getByIdKey, updatedWebsite);
120+
},
121+
onError: (error) => {
122+
console.error('Failed to toggle website privacy:', error);
123+
},
124+
});
125+
}
126+
98127
export function useDeleteWebsite() {
99128
const utils = trpc.useUtils();
100129
return trpc.websites.delete.useMutation({

apps/dashboard/stores/jotai/filterAtoms.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type { DynamicQueryFilter } from '@databuddy/shared';
22
import dayjs from 'dayjs';
33
import { atom } from 'jotai';
4+
import { RECOMMENDED_DEFAULTS } from '../../app/(main)/websites/[id]/_components/utils/tracking-defaults';
5+
import {
6+
enableAllAdvancedTracking,
7+
enableAllBasicTracking,
8+
enableAllOptimization,
9+
} from '../../app/(main)/websites/[id]/_components/utils/tracking-helpers';
10+
import type { TrackingOptions } from '../../app/(main)/websites/[id]/_components/utils/types';
411
// Consider adding nanoid for unique ID generation for complex filters
512
// import { nanoid } from 'nanoid';
613

@@ -324,3 +331,65 @@ export const removeDynamicFilterAtom = atom(
324331
export const clearDynamicFiltersAtom = atom(null, (_get, set) => {
325332
set(dynamicQueryFiltersAtom, []);
326333
});
334+
335+
// --- Tracking Options ---
336+
/**
337+
* Atom for website tracking options configuration.
338+
* Shared across settings and tracking setup tabs.
339+
*/
340+
export const trackingOptionsAtom = atom<TrackingOptions>(RECOMMENDED_DEFAULTS);
341+
342+
/**
343+
* Action atom for updating tracking options.
344+
*/
345+
export const setTrackingOptionsAtom = atom(
346+
null,
347+
(_get, set, newOptions: TrackingOptions) => {
348+
set(trackingOptionsAtom, newOptions);
349+
}
350+
);
351+
352+
/**
353+
* Action atom for toggling a specific tracking option.
354+
*/
355+
export const toggleTrackingOptionAtom = atom(
356+
null,
357+
(get, set, option: keyof TrackingOptions) => {
358+
const current = get(trackingOptionsAtom);
359+
set(trackingOptionsAtom, {
360+
...current,
361+
[option]: !current[option],
362+
});
363+
}
364+
);
365+
366+
/**
367+
* Action atom for resetting tracking options to defaults.
368+
*/
369+
export const resetTrackingOptionsAtom = atom(null, (_get, set) => {
370+
set(trackingOptionsAtom, RECOMMENDED_DEFAULTS);
371+
});
372+
373+
/**
374+
* Action atom for enabling all basic tracking options.
375+
*/
376+
export const enableAllBasicTrackingAtom = atom(null, (get, set) => {
377+
const current = get(trackingOptionsAtom);
378+
set(trackingOptionsAtom, enableAllBasicTracking(current));
379+
});
380+
381+
/**
382+
* Action atom for enabling all advanced tracking options.
383+
*/
384+
export const enableAllAdvancedTrackingAtom = atom(null, (get, set) => {
385+
const current = get(trackingOptionsAtom);
386+
set(trackingOptionsAtom, enableAllAdvancedTracking(current));
387+
});
388+
389+
/**
390+
* Action atom for enabling all optimization options.
391+
*/
392+
export const enableAllOptimizationAtom = atom(null, (get, set) => {
393+
const current = get(trackingOptionsAtom);
394+
set(trackingOptionsAtom, enableAllOptimization(current));
395+
});

0 commit comments

Comments
 (0)