diff --git a/sigap-website/app/_components/admin/settings/setting-dialog.tsx b/sigap-website/app/_components/admin/settings/setting-dialog.tsx
index 9ffb380..4ee403a 100644
--- a/sigap-website/app/_components/admin/settings/setting-dialog.tsx
+++ b/sigap-website/app/_components/admin/settings/setting-dialog.tsx
@@ -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:
Preferences content
,
+ content:
,
},
{
id: "notifications",
icon: IconBell,
title: "Notifications",
- content:
Notifications content
,
- },
- {
- id: "connections",
- icon: IconPlugConnected,
- title: "Connections",
- content:
Connections content
,
+ content:
,
},
],
},
@@ -98,28 +98,16 @@ export function SettingsDialog({
title: "Workspace",
tabs: [
{
- id: "general",
- icon: IconWorld,
- title: "General",
- content:
General content
,
+ id: "import",
+ icon: IconFileImport,
+ title: "Import",
+ content:
,
},
{
- id: "members",
- icon: IconUsers,
- title: "Members",
- content:
Members content
,
- },
- {
- id: "security",
- icon: IconLock,
- title: "Security",
- content:
Security content
,
- },
- {
- id: "identity",
- icon: IconFingerprint,
- title: "Identity",
- content:
Identity content
,
+ id: "export",
+ icon: IconFileExport,
+ title: "Export",
+ content:
Export
,
},
],
},
diff --git a/sigap-website/app/_components/custom-dropdown-switcher.tsx b/sigap-website/app/_components/custom-dropdown-switcher.tsx
new file mode 100644
index 0000000..2e4e67b
--- /dev/null
+++ b/sigap-website/app/_components/custom-dropdown-switcher.tsx
@@ -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
= {
+ value: T;
+ prefix?: LucideIcon;
+ label: string;
+ subLabel?: string;
+ beta?: boolean;
+ isCurrent?: boolean;
+};
+
+type Variant = "outline" | "ghost";
+
+interface DropdownSwitcherProps {
+ options: Option[];
+ 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 = ({
+ 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) => {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isOpen, setIsOpen] = useState(false);
+ const searchInputRef = useRef(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) => {
+ 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 (
+
+
+
+
+ e.preventDefault()}
+ >
+ {searchable && (
+
+
+
+
{
+ if (e.key === "Escape") {
+ e.stopPropagation();
+ setIsOpen(false);
+ }
+ }}
+ disabled={disabled}
+ />
+
+ )}
+
+ {currentOptions.length > 0 && (
+ <>
+
+
+ {currentLabel}
+
+ {currentOptions.map((option) => (
+
+
+ {option.label}
+ {option.subLabel && (
+
+ {option.subLabel}
+
+ )}
+
+
+
+ ))}
+
+
+ >
+ )}
+
+
+
{selectLabel}
+ {selectableOptions.length === 0 ? (
+
+ No timezones found
+
+ ) : (
+ selectableOptions.map((option) => (
+
handleOptionSelect(option.value)}
+ disabled={disabled}
+ >
+
+ {option.label}
+ {option.subLabel && (
+
+ {option.subLabel}
+
+ )}
+
+ {option.beta && (
+
+ BETA
+
+ )}
+
+ ))
+ )}
+
+
+
+ );
+};
+
+export default DropdownSwitcher;
diff --git a/sigap-website/app/_components/theme-switcher.tsx b/sigap-website/app/_components/theme-switcher.tsx
index f503191..dcaf39a 100644
--- a/sigap-website/app/_components/theme-switcher.tsx
+++ b/sigap-website/app/_components/theme-switcher.tsx
@@ -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 = ({
diff --git a/sigap-website/prisma/data/languages.ts b/sigap-website/prisma/data/languages.ts
new file mode 100644
index 0000000..2b9ef35
--- /dev/null
+++ b/sigap-website/prisma/data/languages.ts
@@ -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,
+ },
+];
diff --git a/sigap-website/prisma/data/timezones.ts b/sigap-website/prisma/data/timezones.ts
new file mode 100644
index 0000000..77a28f6
--- /dev/null
+++ b/sigap-website/prisma/data/timezones.ts
@@ -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)",
+ },
+];
diff --git a/sigap-website/utils/cookies-manager.ts b/sigap-website/utils/cookies-manager.ts
new file mode 100644
index 0000000..e71fb64
--- /dev/null
+++ b/sigap-website/utils/cookies-manager.ts
@@ -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();
+ }
+};
diff --git a/sigap-website/utils/notification-cookies-manager.ts b/sigap-website/utils/notification-cookies-manager.ts
new file mode 100644
index 0000000..71ec4da
--- /dev/null
+++ b/sigap-website/utils/notification-cookies-manager.ts
@@ -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();
+ }
+};