add layout for my account, preference, notiication and import
This commit is contained in:
parent
aa52dd0ca4
commit
276a63a4dc
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"files.autoSave": "afterDelay"
|
||||
"files.autoSave": "off"
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ export default async function Layout({
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<InboxDrawer showTitle={true} showAvatar={false} />
|
||||
<ThemeSwitcher showTitle={true} />
|
||||
<ThemeSwitcher showTitle={true} title="Theme" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import React from "react";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Card, CardContent } from "@/app/_components/ui/card";
|
||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { Upload } from "lucide-react";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
IconBrandGoogleAnalytics,
|
||||
IconCsv,
|
||||
IconFileExcel,
|
||||
IconFileWord,
|
||||
IconHtml,
|
||||
IconMarkdown,
|
||||
TablerIcon,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
// Data for import options
|
||||
type ImportOption = {
|
||||
name: string;
|
||||
icon: TablerIcon;
|
||||
beta?: boolean;
|
||||
new?: boolean;
|
||||
sync?: boolean;
|
||||
};
|
||||
|
||||
const importOptions: ImportOption[] = [
|
||||
{ name: "Excel", icon: IconFileExcel },
|
||||
{ name: "Google Sheets", icon: IconBrandGoogleAnalytics },
|
||||
{ name: "CSV", icon: IconCsv },
|
||||
{ name: "Text & Markdown", icon: IconMarkdown },
|
||||
{ name: "Word", icon: IconFileWord },
|
||||
];
|
||||
|
||||
const ImportData = () => {
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-140px)] w-full">
|
||||
<div className="min-h-screen p-8 max-w-4xl mx-auto space-y-8">
|
||||
<div className="space-y-4 mb-4">
|
||||
<h1 className="font-medium">Import data</h1>
|
||||
<Separator />
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold mb-2">
|
||||
Import data from other apps and files into Sigap.
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can import crime data from csv files, text files, and other
|
||||
apps into Sigap.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{importOptions.map((option) => (
|
||||
<Card
|
||||
key={option.name}
|
||||
className="cursor-pointer bg-secondary hover:bg-muted-foreground/15"
|
||||
>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<span className="text-lg">
|
||||
{React.createElement(option.icon)}
|
||||
</span>
|
||||
<div className="flex-1 ml-3">
|
||||
<p className="text-sm font-medium">{option.name}</p>
|
||||
</div>
|
||||
{option.beta && (
|
||||
<Badge className="text-xs text-red-600 w-fit bg-secondary-foreground dark:bg-secondary-foreground hover:bg-secondary-foreground dark:hover:bg-secondary-foreground text-[10px] rounded px-1 py-0">
|
||||
Beta
|
||||
</Badge>
|
||||
)}
|
||||
{option.new && (
|
||||
<Badge className="text-xs text-green-500 w-fit bg-secondary-foreground dark:bg-secondary-foreground hover:bg-secondary-foreground dark:hover:bg-secondary-foreground text-[10px] rounded px-1 py-0">
|
||||
New
|
||||
</Badge>
|
||||
)}
|
||||
{option.sync && (
|
||||
<Badge className="text-xs text-blue-500 w-fit bg-secondary-foreground dark:bg-secondary-foreground hover:bg-secondary-foreground dark:hover:bg-secondary-foreground text-[10px] rounded px-1 py-0">
|
||||
Sync
|
||||
</Badge>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div>
|
||||
<p className="text-sm text-muted-foreground text-center mb-2">
|
||||
Don't see the app you use? Import a ZIP file, and Notion will
|
||||
convert it.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="w-full flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-lg cursor-pointer p-6 hover:border-gray-400"
|
||||
>
|
||||
<Upload className="w-10 h-10 text-gray-500 mb-2" />
|
||||
<span className="text-sm text-gray-500">
|
||||
Drag and drop your ZIP files here
|
||||
</span>
|
||||
<input id="file-upload" type="file" className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportData;
|
|
@ -0,0 +1,150 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Switch } from "@/app/_components/ui/switch";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import {
|
||||
type NotificationPreferences,
|
||||
defaultNotificationPreferences,
|
||||
getNotificationPreferences,
|
||||
saveNotificationPreferences,
|
||||
applyNotificationPreferences,
|
||||
} from "@/utils/notification-cookies-manager";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function NotificationsSetting() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [lastToastTime, setLastToastTime] = useState(0);
|
||||
const [notificationPreferences, setNotificationPreferences] =
|
||||
useState<NotificationPreferences | null>(null);
|
||||
|
||||
// Show toast with debounce to prevent too many notifications
|
||||
const showSavedToast = () => {
|
||||
// const now = Date.now();
|
||||
// if (now - lastToastTime > 2000) {
|
||||
// toast("Preferences saved");
|
||||
// setLastToastTime(now);
|
||||
// }
|
||||
};
|
||||
|
||||
// Load saved preferences on component mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedPreferences = getNotificationPreferences();
|
||||
setNotificationPreferences(savedPreferences);
|
||||
} catch (error) {
|
||||
console.error("Error loading notification preferences:", error);
|
||||
setNotificationPreferences(defaultNotificationPreferences);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Jika masih loading atau notificationPreferences masih null, tampilkan loading
|
||||
if (isLoading || !notificationPreferences) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-140px)]">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Gunakan nilai default jika notificationPreferences belum ada
|
||||
const {
|
||||
mobilePushNotifications,
|
||||
emailNotifications,
|
||||
announcementNotifications,
|
||||
} = notificationPreferences || defaultNotificationPreferences;
|
||||
|
||||
// Generic handler untuk update state dan menyimpan preferensi
|
||||
const updatePreference = (
|
||||
key: keyof NotificationPreferences,
|
||||
value: boolean
|
||||
) => {
|
||||
const newPreferences = { ...notificationPreferences, [key]: value };
|
||||
setNotificationPreferences(newPreferences);
|
||||
saveNotificationPreferences(newPreferences);
|
||||
applyNotificationPreferences(newPreferences);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-140px)] w-full">
|
||||
<div className="min-h-screen p-8 max-w-4xl mx-auto space-y-14">
|
||||
<div>
|
||||
<div className="space-y-4 mb-4">
|
||||
<h1 className="font-medium">Notifications</h1>
|
||||
<Separator />
|
||||
</div>
|
||||
|
||||
{/* Mobile push notification Section */}
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">
|
||||
Mobile push notifications
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Receive push notifications on your mobile device when you're
|
||||
offline.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={mobilePushNotifications}
|
||||
onCheckedChange={(checked) =>
|
||||
updatePreference("mobilePushNotifications", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email notifications Section */}
|
||||
<div>
|
||||
<h2 className="font-medium">Email Notifications</h2>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">
|
||||
Always send email notifications
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Receive emails about activity in your workspace, even when
|
||||
you're active on the app.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={emailNotifications}
|
||||
onCheckedChange={(checked) =>
|
||||
updatePreference("emailNotifications", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">
|
||||
Announcements and Update Emails
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Receive occasional emails about product launches and new
|
||||
features from Notion.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={announcementNotifications}
|
||||
onCheckedChange={(checked) =>
|
||||
updatePreference("announcementNotifications", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,433 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { Switch } from "@/app/_components/ui/switch";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import { ThemeSwitcher } from "../../theme-switcher";
|
||||
import DropdownSwitcher from "../../custom-dropdown-switcher";
|
||||
import {
|
||||
type CookiePreferences,
|
||||
defaultCookiePreferences,
|
||||
getCookiePreferences,
|
||||
saveCookiePreferences,
|
||||
getLanguagePreference,
|
||||
saveLanguagePreference,
|
||||
getTimezonePreference,
|
||||
saveTimezonePreference,
|
||||
getWeekStartPreference,
|
||||
saveWeekStartPreference,
|
||||
getAutoTimezonePreference,
|
||||
saveAutoTimezonePreference,
|
||||
applyCookiePreferences,
|
||||
} from "@/utils/cookies-manager";
|
||||
import { toast } from "sonner";
|
||||
import { initialTimezones, TimezoneType } from "@/prisma/data/timezones";
|
||||
import { languages, LanguageType } from "@/prisma/data/languages";
|
||||
|
||||
export default function PreferencesSettings() {
|
||||
const [language, setLanguage] = useState("en-US");
|
||||
const [languageLabel, setLanguageLabel] = useState("English");
|
||||
const [startWeekOnMonday, setStartWeekOnMonday] = useState(false);
|
||||
const [autoTimezone, setAutoTimezone] = useState(true);
|
||||
const [timezone, setTimezone] = useState("Asia/Jakarta");
|
||||
const [timezoneLabel, setTimezoneLabel] = useState("(GMT+7:00) Jakarta");
|
||||
const [timezones, setTimezones] = useState<LanguageType[]>([]);
|
||||
const [availableLanguages, setAvailableLanguages] = useState<TimezoneType[]>(
|
||||
[]
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [cookiePreferences, setCookiePreferences] = useState<CookiePreferences>(
|
||||
defaultCookiePreferences
|
||||
);
|
||||
const [lastToastTime, setLastToastTime] = useState(0);
|
||||
|
||||
// Show toast with debounce to prevent too many notifications
|
||||
const showSavedToast = () => {
|
||||
const now = Date.now();
|
||||
// Only show toast if it's been at least 2 seconds since the last one
|
||||
// if (now - lastToastTime > 2000) {
|
||||
// toast("Preferences saved");
|
||||
// setLastToastTime(now);
|
||||
// }
|
||||
};
|
||||
|
||||
// Load data when component mounts
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Load cookie preferences
|
||||
const savedCookiePreferences = getCookiePreferences();
|
||||
setCookiePreferences(savedCookiePreferences);
|
||||
|
||||
// Load language preference
|
||||
const savedLanguage = getLanguagePreference();
|
||||
if (savedLanguage) {
|
||||
setLanguage(savedLanguage);
|
||||
}
|
||||
|
||||
// Load week start preference
|
||||
const savedWeekStart = getWeekStartPreference();
|
||||
setStartWeekOnMonday(savedWeekStart);
|
||||
|
||||
// Load auto timezone preference
|
||||
const savedAutoTimezone = getAutoTimezonePreference();
|
||||
setAutoTimezone(savedAutoTimezone);
|
||||
|
||||
// Load timezone preference
|
||||
const savedTimezone = getTimezonePreference();
|
||||
if (savedTimezone) {
|
||||
setTimezone(savedTimezone);
|
||||
}
|
||||
|
||||
// Load timezone and language data
|
||||
if (Array.isArray(initialTimezones)) {
|
||||
setTimezones(initialTimezones);
|
||||
} else {
|
||||
console.warn("Using fallback timezone data");
|
||||
setTimezones([
|
||||
{
|
||||
value: "Asia/Jakarta",
|
||||
prefix: ChevronDown,
|
||||
label: "Jakarta",
|
||||
subLabel: "(GMT+7:00)",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (Array.isArray(languages)) {
|
||||
setAvailableLanguages(languages);
|
||||
} else {
|
||||
console.warn("Using fallback language data");
|
||||
setAvailableLanguages([
|
||||
{
|
||||
value: "en-US",
|
||||
prefix: ChevronDown,
|
||||
label: "English",
|
||||
subLabel: "English (US)",
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
setTimezones([
|
||||
{
|
||||
value: "Asia/Jakarta",
|
||||
prefix: ChevronDown,
|
||||
label: "Jakarta",
|
||||
subLabel: "(GMT+7:00)",
|
||||
},
|
||||
]);
|
||||
setAvailableLanguages([
|
||||
{
|
||||
value: "en-US",
|
||||
prefix: ChevronDown,
|
||||
label: "English",
|
||||
subLabel: "English (US)",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update labels when data is loaded
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
// Set language label
|
||||
const selectedLanguage = availableLanguages.find(
|
||||
(lang) => lang.value === language
|
||||
);
|
||||
if (selectedLanguage) {
|
||||
setLanguageLabel(selectedLanguage.label);
|
||||
}
|
||||
|
||||
// Set timezone label
|
||||
const selectedTimezone = timezones.find((tz) => tz.value === timezone);
|
||||
if (selectedTimezone) {
|
||||
setTimezoneLabel(
|
||||
selectedTimezone.subLabel + " " + selectedTimezone.label
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [isLoading, language, timezone, availableLanguages, timezones]);
|
||||
|
||||
// Detect browser language
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoading &&
|
||||
availableLanguages.length > 0 &&
|
||||
!getLanguagePreference()
|
||||
) {
|
||||
const browserLang = navigator.language;
|
||||
const matchedLang = availableLanguages.find(
|
||||
(lang) =>
|
||||
lang.value === browserLang ||
|
||||
lang.value.split("-")[0] === browserLang.split("-")[0]
|
||||
);
|
||||
|
||||
if (matchedLang) {
|
||||
setLanguage(matchedLang.value);
|
||||
setLanguageLabel(matchedLang.label);
|
||||
// Save the detected language
|
||||
saveLanguagePreference(matchedLang.value);
|
||||
}
|
||||
}
|
||||
}, [isLoading, availableLanguages]);
|
||||
|
||||
// Handle language change
|
||||
const handleLanguageChange = (value: string) => {
|
||||
setLanguage(value);
|
||||
const selectedLanguage = availableLanguages.find(
|
||||
(lang) => lang.value === value
|
||||
);
|
||||
if (selectedLanguage) {
|
||||
setLanguageLabel(selectedLanguage.label);
|
||||
}
|
||||
// Save immediately
|
||||
saveLanguagePreference(value);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
// Handle timezone change
|
||||
const handleTimezoneChange = (value: string) => {
|
||||
setTimezone(value);
|
||||
const selectedTimezone = timezones.find((tz) => tz.value === value);
|
||||
if (selectedTimezone) {
|
||||
setTimezoneLabel(
|
||||
selectedTimezone.subLabel + " " + selectedTimezone.label
|
||||
);
|
||||
}
|
||||
// Save immediately
|
||||
saveTimezonePreference(value);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
// Handle week start change
|
||||
const handleWeekStartChange = (value: boolean) => {
|
||||
setStartWeekOnMonday(value);
|
||||
// Save immediately
|
||||
saveWeekStartPreference(value);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
// Handle auto timezone change
|
||||
const handleAutoTimezoneChange = (value: boolean) => {
|
||||
setAutoTimezone(value);
|
||||
// Save immediately
|
||||
saveAutoTimezonePreference(value);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
// Handle cookie preference changes
|
||||
const handleCookiePreferenceChange = (
|
||||
type: keyof CookiePreferences,
|
||||
value: boolean
|
||||
) => {
|
||||
const newPreferences = { ...cookiePreferences, [type]: value };
|
||||
setCookiePreferences(newPreferences);
|
||||
// Save and apply immediately
|
||||
saveCookiePreferences(newPreferences);
|
||||
applyCookiePreferences(newPreferences);
|
||||
showSavedToast();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-140px)]">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-140px)] w-full">
|
||||
<div className="min-h-screen p-8 max-w-4xl mx-auto space-y-14">
|
||||
<div>
|
||||
<div className="space-y-4 mb-4">
|
||||
<h1 className="font-medium">Preferences</h1>
|
||||
<Separator className="" />
|
||||
</div>
|
||||
|
||||
{/* Appearance Section */}
|
||||
<div className="">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-medium">Appearance</h2>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<ThemeSwitcher
|
||||
showTitle={true}
|
||||
prefix={false}
|
||||
suffix={ChevronDown}
|
||||
variant={"ghost"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Customize how the application looks on your device.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language & Time Section */}
|
||||
<div className="">
|
||||
<div className="space-y-4 mb-4">
|
||||
<h2 className="font-medium">Language & Time</h2>
|
||||
<Separator className="my-4" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Language Subsection */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Language</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Change the language used in the user interface.
|
||||
</p>
|
||||
</div>
|
||||
<DropdownSwitcher
|
||||
key={language}
|
||||
options={availableLanguages}
|
||||
prefix={false}
|
||||
suffix={ChevronDown}
|
||||
showTitle={true}
|
||||
variant="ghost"
|
||||
title={languageLabel}
|
||||
selectedValue={language}
|
||||
searchable={true}
|
||||
searchPlaceholder="Search languages..."
|
||||
onChange={handleLanguageChange}
|
||||
currentLabel="Current language"
|
||||
selectLabel="Select a language"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Start week on Monday Subsection */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Start week on Monday</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This will change how all calendars in your app look.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={startWeekOnMonday}
|
||||
onCheckedChange={handleWeekStartChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Auto Timezone Subsection */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">
|
||||
Set timezone automatically using your location
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Reminders, notifications and emails are delivered based on
|
||||
your time zone.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoTimezone}
|
||||
onCheckedChange={handleAutoTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Timezone Subsection */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Timezone</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Current timezone setting.
|
||||
</p>
|
||||
</div>
|
||||
<DropdownSwitcher
|
||||
key={timezone}
|
||||
options={timezones}
|
||||
prefix={false}
|
||||
suffix={ChevronDown}
|
||||
showTitle={true}
|
||||
variant="ghost"
|
||||
title={timezoneLabel}
|
||||
selectedValue={timezone}
|
||||
searchable={true}
|
||||
searchPlaceholder="Search for a timezone..."
|
||||
onChange={handleTimezoneChange}
|
||||
currentLabel="Current timezone"
|
||||
selectLabel="Select a timezone"
|
||||
disabled={autoTimezone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Privacy Section */}
|
||||
<div className="">
|
||||
<div className="space-y-4 mb-4">
|
||||
<h2 className="font-medium">Privacy</h2>
|
||||
<Separator className="" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{/* Cookies Settings */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Necessary Cookies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Required for the website to function properly. Cannot be
|
||||
disabled.
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={true} disabled className="opacity-50" />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Functional Cookies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enable enhanced functionality and personalization.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={cookiePreferences.functional}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCookiePreferenceChange("functional", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Analytics Cookies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Help us improve by collecting anonymous information about how
|
||||
you use the site.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={cookiePreferences.analytics}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCookiePreferenceChange("analytics", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">Marketing Cookies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Used to display personalized advertisements based on your
|
||||
browsing habits.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={cookiePreferences.marketing || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCookiePreferenceChange("marketing", checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
|
@ -107,12 +107,14 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
|||
|
||||
return (
|
||||
<ScrollArea className="h-[calc(100vh-140px)] w-full ">
|
||||
<div className="space-y-16 px-20 py-10">
|
||||
<div className="space-y-14 min-h-screen p-8 max-w-4xl mx-auto">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 mb-4">
|
||||
<h3 className="text-lg font-semibold">Account</h3>
|
||||
<Separator className="" />
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className="relative cursor-pointer group"
|
||||
|
@ -186,8 +188,10 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
|||
</Form>
|
||||
|
||||
<div className="">
|
||||
<div className="space-y-4 mb-4">
|
||||
<h3 className="text-base font-medium">Account security</h3>
|
||||
<Separator className="my-2" />
|
||||
<Separator className="" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
@ -239,8 +243,10 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-medium">Support</h3>
|
||||
<Separator className="my-2" />
|
||||
<div className="space-y-4 mb-4">
|
||||
<h3 className="text-base font-medium">Notifications</h3>
|
||||
<Separator className="" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
} from "@/app/_components/ui/avatar";
|
||||
import {
|
||||
IconBell,
|
||||
IconFileExport,
|
||||
IconFileImport,
|
||||
IconFingerprint,
|
||||
IconLock,
|
||||
IconPlugConnected,
|
||||
|
@ -29,6 +31,10 @@ import { ProfileSettings } from "./profile-settings";
|
|||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
import NotificationsSetting from "./notification-settings";
|
||||
import PreferencesSettings from "./preference-settings";
|
||||
import ImportData from "./import-data";
|
||||
|
||||
interface SettingsDialogProps {
|
||||
user: User | null;
|
||||
trigger: React.ReactNode;
|
||||
|
@ -78,19 +84,13 @@ export function SettingsDialog({
|
|||
id: "preferences",
|
||||
icon: IconSettings,
|
||||
title: "Preferences",
|
||||
content: <div>Preferences content</div>,
|
||||
content: <PreferencesSettings />,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
icon: IconBell,
|
||||
title: "Notifications",
|
||||
content: <div>Notifications content</div>,
|
||||
},
|
||||
{
|
||||
id: "connections",
|
||||
icon: IconPlugConnected,
|
||||
title: "Connections",
|
||||
content: <div>Connections content</div>,
|
||||
content: <NotificationsSetting />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -98,28 +98,16 @@ export function SettingsDialog({
|
|||
title: "Workspace",
|
||||
tabs: [
|
||||
{
|
||||
id: "general",
|
||||
icon: IconWorld,
|
||||
title: "General",
|
||||
content: <div>General content</div>,
|
||||
id: "import",
|
||||
icon: IconFileImport,
|
||||
title: "Import",
|
||||
content: <ImportData />,
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
icon: IconUsers,
|
||||
title: "Members",
|
||||
content: <div>Members content</div>,
|
||||
},
|
||||
{
|
||||
id: "security",
|
||||
icon: IconLock,
|
||||
title: "Security",
|
||||
content: <div>Security content</div>,
|
||||
},
|
||||
{
|
||||
id: "identity",
|
||||
icon: IconFingerprint,
|
||||
title: "Identity",
|
||||
content: <div>Identity content</div>,
|
||||
id: "export",
|
||||
icon: IconFileExport,
|
||||
title: "Export",
|
||||
content: <div>Export</div>,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
"use client";
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Check, Search, type LucideIcon } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/app/_components/ui/dropdown-menu";
|
||||
import { Badge } from "@/app/_components/ui/badge";
|
||||
import { Input } from "@/app/_components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Option<T> = {
|
||||
value: T;
|
||||
prefix?: LucideIcon;
|
||||
label: string;
|
||||
subLabel?: string;
|
||||
beta?: boolean;
|
||||
isCurrent?: boolean;
|
||||
};
|
||||
|
||||
type Variant = "outline" | "ghost";
|
||||
|
||||
interface DropdownSwitcherProps<T> {
|
||||
options: Option<T>[];
|
||||
selectedValue: T;
|
||||
onChange: (value: T) => void;
|
||||
showTitle?: boolean;
|
||||
title?: string;
|
||||
prefix?: boolean;
|
||||
suffix?: LucideIcon;
|
||||
variant?: Variant;
|
||||
searchable?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
currentLabel?: string;
|
||||
selectLabel?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
const DropdownSwitcher = <T extends string>({
|
||||
options,
|
||||
selectedValue,
|
||||
onChange,
|
||||
showTitle = false,
|
||||
title = "Select",
|
||||
prefix = true,
|
||||
suffix,
|
||||
variant = "outline",
|
||||
searchable = false,
|
||||
searchPlaceholder = "Search...",
|
||||
currentLabel = "Current timezone",
|
||||
selectLabel = "Select a timezone",
|
||||
disabled = false,
|
||||
}: DropdownSwitcherProps<T>) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const currentOption = useMemo(
|
||||
() =>
|
||||
options.find((option) => option.value === selectedValue) || options[0],
|
||||
[selectedValue, options]
|
||||
);
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
if (!searchQuery.trim()) return options;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.label.toLowerCase().includes(query) ||
|
||||
option.value.toLowerCase().includes(query) ||
|
||||
(option.subLabel && option.subLabel.toLowerCase().includes(query))
|
||||
);
|
||||
}, [options, searchQuery]);
|
||||
|
||||
const { currentOptions, selectableOptions } = useMemo(
|
||||
() => ({
|
||||
currentOptions: filteredOptions.filter(
|
||||
(opt) => opt.value === selectedValue
|
||||
),
|
||||
selectableOptions: filteredOptions.filter(
|
||||
(opt) => opt.value !== selectedValue
|
||||
),
|
||||
}),
|
||||
[filteredOptions, selectedValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchable) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, 10);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [isOpen, searchable]);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchQuery(e.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setIsOpen(open);
|
||||
}, []);
|
||||
|
||||
const handleOptionSelect = useCallback(
|
||||
(value: T) => {
|
||||
onChange(value);
|
||||
setIsOpen(false);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleSearchInteraction = useCallback(
|
||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={showTitle ? "sm" : "icon"}
|
||||
className={cn(
|
||||
variant !== "outline" ? "" : "border-2",
|
||||
showTitle ? "flex justify-center items-center" : ""
|
||||
)}
|
||||
aria-label={`Current selection: ${selectedValue}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
{prefix && currentOption.prefix && (
|
||||
<currentOption.prefix
|
||||
size={ICON_SIZE}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
{showTitle && (
|
||||
<span className="text-muted-foreground font-medium">{title}</span>
|
||||
)}
|
||||
{!prefix &&
|
||||
suffix &&
|
||||
React.createElement(suffix, {
|
||||
size: ICON_SIZE,
|
||||
className: "text-muted-foreground",
|
||||
})}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[300px] max-h-[400px] overflow-auto"
|
||||
align="start"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{searchable && (
|
||||
<div
|
||||
className="sticky top-0 bg-background z-50 px-2 py-2"
|
||||
onClick={handleSearchInteraction}
|
||||
onMouseDown={handleSearchInteraction}
|
||||
>
|
||||
<div className="relative"></div>
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
className="pl-8 h-9"
|
||||
onClick={handleSearchInteraction}
|
||||
onMouseDown={handleSearchInteraction}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentOptions.length > 0 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
{currentLabel}
|
||||
</p>
|
||||
{currentOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
className="flex items-center justify-between py-0"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
{option.subLabel && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option.subLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="px-2 py-1.5">
|
||||
<p className="text-sm text-muted-foreground mb-1">{selectLabel}</p>
|
||||
{selectableOptions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No timezones found
|
||||
</p>
|
||||
) : (
|
||||
selectableOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
className="flex items-center justify-between py-2"
|
||||
onClick={() => handleOptionSelect(option.value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{option.label}</span>
|
||||
{option.subLabel && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{option.subLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{option.beta && (
|
||||
<Badge className="w-fit bg-secondary-foreground dark:bg-muted hover:bg-secondary-foreground dark:hover:bg-muted text-[10px] rounded px-1 py-0">
|
||||
BETA
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownSwitcher;
|
|
@ -14,8 +14,20 @@ import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react";
|
|||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
|
||||
type Variant =
|
||||
| "link"
|
||||
| "outline"
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "secondary"
|
||||
| "ghost";
|
||||
|
||||
interface ThemeSwitcherComponentProps {
|
||||
showTitle?: boolean;
|
||||
title?: string;
|
||||
prefix?: boolean;
|
||||
suffix?: LucideIcon;
|
||||
variant?: Variant;
|
||||
}
|
||||
|
||||
type ThemeOption = {
|
||||
|
@ -34,6 +46,10 @@ const themeOptions: ThemeOption[] = [
|
|||
|
||||
const ThemeSwitcherComponent = ({
|
||||
showTitle = false,
|
||||
title,
|
||||
prefix = true,
|
||||
suffix,
|
||||
variant = "outline",
|
||||
}: ThemeSwitcherComponentProps) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
@ -56,18 +72,28 @@ const ThemeSwitcherComponent = ({
|
|||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={variant}
|
||||
size={showTitle ? "sm" : "icon"}
|
||||
className={`border-2 ${showTitle ? "flex justify-center items-center" : ""}`}
|
||||
aria-label={`Current theme: theme`}
|
||||
className={`${variant != "outline" ? "" : "border-2"} ${showTitle ? "flex justify-center items-center" : ""}`}
|
||||
aria-label={`Current theme: ${theme}`}
|
||||
>
|
||||
{prefix && (
|
||||
<currentTheme.icon
|
||||
size={ICON_SIZE}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
{showTitle && (
|
||||
<span className="text-muted-foreground font-medium">Theme</span>
|
||||
)}
|
||||
{showTitle && (
|
||||
<span className="text-muted-foreground font-medium">
|
||||
{title || currentTheme.label}
|
||||
</span>
|
||||
)}
|
||||
{!prefix &&
|
||||
suffix &&
|
||||
React.createElement(suffix, {
|
||||
size: ICON_SIZE,
|
||||
className: "text-muted-foreground",
|
||||
})}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-content" align="start">
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { ChevronDown, LucideIcon } from "lucide-react";
|
||||
|
||||
export type LanguageType = {
|
||||
value: string;
|
||||
prefix?: LucideIcon;
|
||||
label: string;
|
||||
subLabel: string;
|
||||
beta?: boolean;
|
||||
};
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
value: "en-US",
|
||||
prefix: ChevronDown,
|
||||
label: "English",
|
||||
subLabel: "English (US)",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
value: "id-ID",
|
||||
prefix: ChevronDown,
|
||||
label: "Indonesian",
|
||||
subLabel: "Indonesia",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
value: "ja-JP",
|
||||
prefix: ChevronDown,
|
||||
label: "日本語",
|
||||
subLabel: "Japanese",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
value: "ko-KR",
|
||||
prefix: ChevronDown,
|
||||
label: "한국어",
|
||||
subLabel: "Korean",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
value: "zh-CN",
|
||||
prefix: ChevronDown,
|
||||
label: "中文",
|
||||
subLabel: "Chinese (Simplified)",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
value: "es-419",
|
||||
prefix: ChevronDown,
|
||||
label: "Español (Latinoamérica)",
|
||||
subLabel: "Spanish (Latin America)",
|
||||
beta: true,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,42 @@
|
|||
import { ChevronDown, LucideIcon } from "lucide-react";
|
||||
|
||||
export type TimezoneType = {
|
||||
value: string;
|
||||
prefix?: LucideIcon;
|
||||
label: string;
|
||||
subLabel: string;
|
||||
};
|
||||
|
||||
// Initial timezone options
|
||||
export const initialTimezones = [
|
||||
{
|
||||
value: "Asia/Jakarta",
|
||||
prefix: ChevronDown,
|
||||
label: "Jakarta",
|
||||
subLabel: "(GMT+7:00)",
|
||||
},
|
||||
{
|
||||
value: "Asia/Singapore",
|
||||
prefix: ChevronDown,
|
||||
label: "Singapore",
|
||||
subLabel: "(GMT+8:00)",
|
||||
},
|
||||
{
|
||||
value: "Asia/Tokyo",
|
||||
prefix: ChevronDown,
|
||||
label: "Tokyo",
|
||||
subLabel: "(GMT+9:00)",
|
||||
},
|
||||
{
|
||||
value: "Australia/Adelaide",
|
||||
prefix: ChevronDown,
|
||||
label: "Adelaide",
|
||||
subLabel: "(GMT+9:30)",
|
||||
},
|
||||
{
|
||||
value: "Australia/Sydney",
|
||||
prefix: ChevronDown,
|
||||
label: "Sydney",
|
||||
subLabel: "(GMT+10:00)",
|
||||
},
|
||||
];
|
|
@ -0,0 +1,162 @@
|
|||
// Cookie management utility functions
|
||||
|
||||
// Cookie types
|
||||
export type CookiePreferences = {
|
||||
necessary: boolean;
|
||||
functional: boolean;
|
||||
analytics: boolean;
|
||||
marketing?: boolean;
|
||||
};
|
||||
|
||||
// Default cookie preferences
|
||||
export const defaultCookiePreferences: CookiePreferences = {
|
||||
necessary: true, // Always true, cannot be disabled
|
||||
functional: false,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
};
|
||||
|
||||
// Cookie names
|
||||
const COOKIE_PREFERENCES_KEY = "cookie-preferences";
|
||||
const LANGUAGE_PREFERENCE_KEY = "language-preference";
|
||||
const TIMEZONE_PREFERENCE_KEY = "timezone-preference";
|
||||
const WEEK_START_PREFERENCE_KEY = "week-start-preference";
|
||||
const AUTO_TIMEZONE_PREFERENCE_KEY = "auto-timezone-preference";
|
||||
|
||||
// Helper to set a cookie with expiration
|
||||
export const setCookie = (
|
||||
name: string,
|
||||
value: string,
|
||||
days: number = 365
|
||||
): void => {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
const expires = `; expires=${date.toUTCString()}`;
|
||||
document.cookie = `${name}=${value}${expires}; path=/; SameSite=Lax`;
|
||||
};
|
||||
|
||||
// Helper to get a cookie by name
|
||||
export const getCookie = (name: string): string | null => {
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
const nameEQ = `${name}=`;
|
||||
const ca = document.cookie.split(";");
|
||||
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper to delete a cookie
|
||||
export const deleteCookie = (name: string): void => {
|
||||
setCookie(name, "", -1);
|
||||
};
|
||||
|
||||
// Save cookie preferences
|
||||
export const saveCookiePreferences = (preferences: CookiePreferences): void => {
|
||||
// Always ensure necessary cookies are enabled
|
||||
const prefsToSave = {
|
||||
...preferences,
|
||||
necessary: true,
|
||||
};
|
||||
|
||||
setCookie(COOKIE_PREFERENCES_KEY, JSON.stringify(prefsToSave));
|
||||
};
|
||||
|
||||
// Get cookie preferences
|
||||
export const getCookiePreferences = (): CookiePreferences => {
|
||||
try {
|
||||
const preferences = getCookie(COOKIE_PREFERENCES_KEY);
|
||||
if (preferences) {
|
||||
return { ...defaultCookiePreferences, ...JSON.parse(preferences) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing cookie preferences:", error);
|
||||
}
|
||||
|
||||
return defaultCookiePreferences;
|
||||
};
|
||||
|
||||
// Save language preference
|
||||
export const saveLanguagePreference = (language: string): void => {
|
||||
setCookie(LANGUAGE_PREFERENCE_KEY, language);
|
||||
};
|
||||
|
||||
// Get language preference
|
||||
export const getLanguagePreference = (): string | null => {
|
||||
return getCookie(LANGUAGE_PREFERENCE_KEY);
|
||||
};
|
||||
|
||||
// Save timezone preference
|
||||
export const saveTimezonePreference = (timezone: string): void => {
|
||||
setCookie(TIMEZONE_PREFERENCE_KEY, timezone);
|
||||
};
|
||||
|
||||
// Get timezone preference
|
||||
export const getTimezonePreference = (): string | null => {
|
||||
return getCookie(TIMEZONE_PREFERENCE_KEY);
|
||||
};
|
||||
|
||||
// Save week start preference
|
||||
export const saveWeekStartPreference = (startOnMonday: boolean): void => {
|
||||
setCookie(WEEK_START_PREFERENCE_KEY, startOnMonday ? "monday" : "sunday");
|
||||
};
|
||||
|
||||
// Get week start preference
|
||||
export const getWeekStartPreference = (): boolean => {
|
||||
const pref = getCookie(WEEK_START_PREFERENCE_KEY);
|
||||
return pref === "monday";
|
||||
};
|
||||
|
||||
// Save auto timezone preference
|
||||
export const saveAutoTimezonePreference = (auto: boolean): void => {
|
||||
setCookie(AUTO_TIMEZONE_PREFERENCE_KEY, auto ? "true" : "false");
|
||||
};
|
||||
|
||||
// Get auto timezone preference
|
||||
export const getAutoTimezonePreference = (): boolean => {
|
||||
const pref = getCookie(AUTO_TIMEZONE_PREFERENCE_KEY);
|
||||
return pref === "true";
|
||||
};
|
||||
|
||||
// Apply cookie preferences to the application
|
||||
export const applyCookiePreferences = (
|
||||
preferences: CookiePreferences
|
||||
): void => {
|
||||
// This function would implement the actual cookie policy enforcement
|
||||
// For example, enabling/disabling analytics scripts based on preferences
|
||||
|
||||
if (preferences.analytics) {
|
||||
// Enable analytics scripts
|
||||
console.log("Analytics cookies enabled");
|
||||
// Example: initAnalytics();
|
||||
} else {
|
||||
// Disable analytics scripts
|
||||
console.log("Analytics cookies disabled");
|
||||
// Example: disableAnalytics();
|
||||
}
|
||||
|
||||
if (preferences.functional) {
|
||||
// Enable functional cookies
|
||||
console.log("Functional cookies enabled");
|
||||
// Example: enableFunctionalFeatures();
|
||||
} else {
|
||||
// Disable functional cookies
|
||||
console.log("Functional cookies disabled");
|
||||
// Example: disableFunctionalFeatures();
|
||||
}
|
||||
|
||||
if (preferences.marketing) {
|
||||
// Enable marketing cookies
|
||||
console.log("Marketing cookies enabled");
|
||||
// Example: enableMarketingFeatures();
|
||||
} else {
|
||||
// Disable marketing cookies
|
||||
console.log("Marketing cookies disabled");
|
||||
// Example: disableMarketingFeatures();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,101 @@
|
|||
// Notification preferences cookie management
|
||||
|
||||
// Notification preferences type
|
||||
export type NotificationPreferences = {
|
||||
mobilePushNotifications: boolean;
|
||||
emailNotifications: boolean;
|
||||
announcementNotifications: boolean;
|
||||
};
|
||||
|
||||
// Default notification preferences
|
||||
export const defaultNotificationPreferences: NotificationPreferences = {
|
||||
mobilePushNotifications: true,
|
||||
emailNotifications: true,
|
||||
announcementNotifications: false,
|
||||
};
|
||||
|
||||
// Cookie names
|
||||
const NOTIFICATION_PREFERENCES_KEY = "notification-preferences";
|
||||
|
||||
// Helper to set a cookie with expiration
|
||||
export const setCookie = (name: string, value: string, days = 365): void => {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
const expires = `; expires=${date.toUTCString()}`;
|
||||
document.cookie = `${name}=${value}${expires}; path=/; SameSite=Lax`;
|
||||
};
|
||||
|
||||
// Helper to get a cookie by name
|
||||
export const getCookie = (name: string): string | null => {
|
||||
if (typeof document === "undefined") return null;
|
||||
|
||||
const nameEQ = `${name}=`;
|
||||
const ca = document.cookie.split(";");
|
||||
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Save notification preferences
|
||||
export const saveNotificationPreferences = (
|
||||
preferences: NotificationPreferences
|
||||
): void => {
|
||||
setCookie(NOTIFICATION_PREFERENCES_KEY, JSON.stringify(preferences));
|
||||
};
|
||||
|
||||
// Get notification preferences
|
||||
export const getNotificationPreferences = (): NotificationPreferences => {
|
||||
try {
|
||||
const preferences = getCookie(NOTIFICATION_PREFERENCES_KEY);
|
||||
if (preferences) {
|
||||
return { ...defaultNotificationPreferences, ...JSON.parse(preferences) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing notification preferences:", error);
|
||||
}
|
||||
|
||||
return defaultNotificationPreferences;
|
||||
};
|
||||
|
||||
// Apply notification preferences to the application
|
||||
export const applyNotificationPreferences = (
|
||||
preferences: NotificationPreferences
|
||||
): void => {
|
||||
// This function would implement the actual notification settings
|
||||
// For example, enabling/disabling push notifications
|
||||
|
||||
if (preferences.mobilePushNotifications) {
|
||||
// Enable push notifications
|
||||
console.log("Push notifications enabled");
|
||||
// Example: requestPushPermission();
|
||||
} else {
|
||||
// Disable push notifications
|
||||
console.log("Push notifications disabled");
|
||||
// Example: disablePushNotifications();
|
||||
}
|
||||
|
||||
if (preferences.emailNotifications) {
|
||||
// Enable email notifications
|
||||
console.log("Email notifications enabled");
|
||||
// Example: enableEmailNotifications();
|
||||
} else {
|
||||
// Disable email notifications
|
||||
console.log("Email notifications disabled");
|
||||
// Example: disableEmailNotifications();
|
||||
}
|
||||
|
||||
if (preferences.announcementNotifications) {
|
||||
// Enable announcement notifications
|
||||
console.log("Announcement notifications enabled");
|
||||
// Example: subscribeToAnnouncementEmails();
|
||||
} else {
|
||||
// Disable announcement notifications
|
||||
console.log("Announcement notifications disabled");
|
||||
// Example: unsubscribeFromAnnouncementEmails();
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue