Next steps
diff --git a/sigap-website/components/action-search-bar.tsx b/sigap-website/app/_components/action-search-bar.tsx
similarity index 98%
rename from sigap-website/components/action-search-bar.tsx
rename to sigap-website/app/_components/action-search-bar.tsx
index 3692754..1c58883 100644
--- a/sigap-website/components/action-search-bar.tsx
+++ b/sigap-website/app/_components/action-search-bar.tsx
@@ -6,7 +6,7 @@ import React, {
forwardRef,
useImperativeHandle,
} from "react";
-import { Input } from "@/components/ui/input";
+import { Input } from "@/app/_components/ui/input";
import { motion, AnimatePresence } from "framer-motion";
import {
Search,
@@ -17,7 +17,7 @@ import {
PlaneTakeoff,
AudioLines,
} from "lucide-react";
-import useDebounce from "@/hooks/use-debounce";
+import useDebounce from "@/app/_hooks/use-debounce";
interface Action {
id: string;
diff --git a/sigap-website/components/admin/app-sidebar.tsx b/sigap-website/app/_components/admin/app-sidebar.tsx
similarity index 77%
rename from sigap-website/components/admin/app-sidebar.tsx
rename to sigap-website/app/_components/admin/app-sidebar.tsx
index ac92040..9fc5ff2 100644
--- a/sigap-website/components/admin/app-sidebar.tsx
+++ b/sigap-website/app/_components/admin/app-sidebar.tsx
@@ -2,9 +2,9 @@
import * as React from "react";
-import { NavMain } from "@/components/admin/navigations/nav-main";
-import { NavReports } from "@/components/admin/navigations/nav-report";
-import { NavUser } from "@/components/admin/navigations/nav-user";
+import { NavMain } from "@/app/_components/admin/navigations/nav-main";
+import { NavReports } from "@/app/_components/admin/navigations/nav-report";
+import { NavUser } from "@/app/_components/admin/navigations/nav-user";
import {
Sidebar,
@@ -12,14 +12,13 @@ import {
SidebarFooter,
SidebarHeader,
SidebarRail,
-} from "@/components/ui/sidebar";
+} from "@/app/_components/ui/sidebar";
import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../team-switcher";
import { Profile, User } from "@/src/models/users/users.model";
-import { getCurrentUser } from "@/app/protected/(admin)/dashboard/user-management/action";
-
+import { getCurrentUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
export function AppSidebar({ ...props }: React.ComponentProps
) {
const [user, setUser] = React.useState(null);
@@ -30,7 +29,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
try {
setIsLoading(true);
const userData = await getCurrentUser();
- setUser(userData);
+ setUser(userData.data.user);
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
diff --git a/sigap-website/components/admin/map/mapbox-view.tsx b/sigap-website/app/_components/admin/map/mapbox-view.tsx
similarity index 100%
rename from sigap-website/components/admin/map/mapbox-view.tsx
rename to sigap-website/app/_components/admin/map/mapbox-view.tsx
diff --git a/sigap-website/components/admin/navigations/nav-main.tsx b/sigap-website/app/_components/admin/navigations/nav-main.tsx
similarity index 96%
rename from sigap-website/components/admin/navigations/nav-main.tsx
rename to sigap-website/app/_components/admin/navigations/nav-main.tsx
index 51efb9c..1a22b2d 100644
--- a/sigap-website/components/admin/navigations/nav-main.tsx
+++ b/sigap-website/app/_components/admin/navigations/nav-main.tsx
@@ -6,7 +6,7 @@ import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
-} from "@/components/ui/collapsible";
+} from "@/app/_components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -14,11 +14,11 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
-} from "@/components/ui/sidebar";
+} from "@/app/_components/ui/sidebar";
import * as TablerIcons from "@tabler/icons-react";
-import { useNavigations } from "@/hooks/use-navigations";
+import { useNavigations } from "@/app/_hooks/use-navigations";
interface SubSubItem {
title: string;
diff --git a/sigap-website/components/admin/navigations/nav-pre-main.tsx b/sigap-website/app/_components/admin/navigations/nav-pre-main.tsx
similarity index 92%
rename from sigap-website/components/admin/navigations/nav-pre-main.tsx
rename to sigap-website/app/_components/admin/navigations/nav-pre-main.tsx
index 5b7b4a8..aac2e4c 100644
--- a/sigap-website/components/admin/navigations/nav-pre-main.tsx
+++ b/sigap-website/app/_components/admin/navigations/nav-pre-main.tsx
@@ -8,8 +8,8 @@ import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
-} from "@/components/ui/sidebar";
-import { useNavigations } from "@/hooks/use-navigations";
+} from "@/app/_components/ui/sidebar";
+import { useNavigations } from "@/app/_hooks/use-navigations";
import { Search, Bot, Home } from "lucide-react";
import { IconHome, IconRobot, IconSearch } from "@tabler/icons-react";
diff --git a/sigap-website/components/admin/navigations/nav-report.tsx b/sigap-website/app/_components/admin/navigations/nav-report.tsx
similarity index 96%
rename from sigap-website/components/admin/navigations/nav-report.tsx
rename to sigap-website/app/_components/admin/navigations/nav-report.tsx
index 3a967d0..77b4c00 100644
--- a/sigap-website/components/admin/navigations/nav-report.tsx
+++ b/sigap-website/app/_components/admin/navigations/nav-report.tsx
@@ -12,7 +12,7 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+} from "@/app/_components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
@@ -21,7 +21,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
-} from "@/components/ui/sidebar";
+} from "@/app/_components/ui/sidebar";
import * as TablerIcons from "@tabler/icons-react";
diff --git a/sigap-website/components/admin/navigations/nav-user.tsx b/sigap-website/app/_components/admin/navigations/nav-user.tsx
similarity index 72%
rename from sigap-website/components/admin/navigations/nav-user.tsx
rename to sigap-website/app/_components/admin/navigations/nav-user.tsx
index 3c97646..583bd22 100644
--- a/sigap-website/components/admin/navigations/nav-user.tsx
+++ b/sigap-website/app/_components/admin/navigations/nav-user.tsx
@@ -1,8 +1,13 @@
"use client";
+import { useState } from "react";
import { ChevronsUpDown } from "lucide-react";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/app/_components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,24 +16,21 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+} from "@/app/_components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
-} from "@/components/ui/sidebar";
-import {
- IconBadgeCc,
- IconBell,
- IconCreditCard,
- IconLogout,
- IconSparkles,
-} from "@tabler/icons-react";
-import { Profile, User } from "@/src/models/users/users.model";
+} from "@/app/_components/ui/sidebar";
+import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
+import type { User } from "@/src/models/users/users.model";
+import { signOut } from "@/app/(auth-pages)/action";
+import { SettingsDialog } from "../settings/setting-dialog";
export function NavUser({ user }: { user: User | null }) {
const { isMobile } = useSidebar();
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
// Use profile data with fallbacks
const firstName = user?.profile?.first_name || "";
@@ -54,6 +56,12 @@ export function NavUser({ user }: { user: User | null }) {
return "U";
};
+ // Handle dialog close after successful profile update
+ const handleProfileUpdateSuccess = () => {
+ setIsDialogOpen(false);
+ // You might want to refresh the user data here
+ };
+
return (
@@ -101,28 +109,30 @@ export function NavUser({ user }: { user: User | null }) {
-
+
Upgrade to Pro
-
-
- Account
-
-
-
- Billing
-
-
-
- Notifications
-
+ {
+ e.preventDefault();
+ }}
+ >
+
+ Settings
+
+ }
+ />
-
-
+
+
Log out
diff --git a/sigap-website/app/_components/admin/navigations/profile-form.tsx b/sigap-website/app/_components/admin/navigations/profile-form.tsx
new file mode 100644
index 0000000..609e231
--- /dev/null
+++ b/sigap-website/app/_components/admin/navigations/profile-form.tsx
@@ -0,0 +1,281 @@
+"use client";
+
+import type React from "react";
+
+import { useState, useRef } from "react";
+import { z } from "zod";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import type { User } from "@/src/models/users/users.model";
+
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/app/_components/ui/form";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/app/_components/ui/avatar";
+import { Input } from "@/app/_components/ui/input";
+import { Textarea } from "@/app/_components/ui/textarea";
+import { Button } from "@/app/_components/ui/button";
+import { Label } from "@/app/_components/ui/label";
+import { ImageIcon, Loader2 } from "lucide-react";
+import { createClient } from "@/utils/supabase/client";
+
+// Profile update form schema
+const profileFormSchema = z.object({
+ first_name: z.string().nullable().optional(),
+ last_name: z.string().nullable().optional(),
+ bio: z.string().nullable().optional(),
+ avatar: z.string().nullable().optional(),
+});
+
+type ProfileFormValues = z.infer;
+
+interface ProfileFormProps {
+ user: User | null;
+ onSuccess?: () => void;
+}
+
+export function ProfileForm({ user, onSuccess }: ProfileFormProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [avatarPreview, setAvatarPreview] = useState(
+ user?.profile?.avatar || null
+ );
+ const fileInputRef = useRef(null);
+ const supabase = createClient();
+
+ // Use profile data with fallbacks
+ const firstName = user?.profile?.first_name || "";
+ const lastName = user?.profile?.last_name || "";
+ const userEmail = user?.email || "";
+ const userBio = user?.profile?.bio || "";
+
+ const getFullName = () => {
+ return `${firstName} ${lastName}`.trim() || "User";
+ };
+
+ // Generate initials for avatar fallback
+ const getInitials = () => {
+ if (firstName && lastName) {
+ return `${firstName[0]}${lastName[0]}`.toUpperCase();
+ }
+ if (firstName) {
+ return firstName[0].toUpperCase();
+ }
+ if (userEmail) {
+ return userEmail[0].toUpperCase();
+ }
+ return "U";
+ };
+
+ // Setup form with react-hook-form and zod validation
+ const form = useForm({
+ resolver: zodResolver(profileFormSchema),
+ defaultValues: {
+ first_name: firstName || "",
+ last_name: lastName || "",
+ bio: userBio || "",
+ avatar: user?.profile?.avatar || "",
+ },
+ });
+
+ // Handle avatar file upload
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !user?.id) return;
+
+ try {
+ setIsLoading(true);
+
+ // Create a preview of the selected image
+ const objectUrl = URL.createObjectURL(file);
+ setAvatarPreview(objectUrl);
+
+ // Upload to Supabase Storage
+ const fileExt = file.name.split(".").pop();
+ const fileName = `${user.id}-${Date.now()}.${fileExt}`;
+ const filePath = `avatars/${fileName}`;
+
+ const { error: uploadError, data } = await supabase.storage
+ .from("profiles")
+ .upload(filePath, file, {
+ upsert: true,
+ contentType: file.type,
+ });
+
+ if (uploadError) {
+ throw uploadError;
+ }
+
+ // Get the public URL
+ const {
+ data: { publicUrl },
+ } = supabase.storage.from("profiles").getPublicUrl(filePath);
+
+ // Update the form value
+ form.setValue("avatar", publicUrl);
+ } catch (error) {
+ console.error("Error uploading avatar:", error);
+ // Revert to previous avatar if upload fails
+ setAvatarPreview(user?.profile?.avatar || null);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Trigger file input click
+ const handleAvatarClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ // Handle form submission
+ async function onSubmit(data: ProfileFormValues) {
+ try {
+ if (!user?.id) return;
+
+ // Update profile in database
+ const { error } = await supabase
+ .from("profiles")
+ .update({
+ first_name: data.first_name,
+ last_name: data.last_name,
+ bio: data.bio,
+ avatar: data.avatar,
+ })
+ .eq("user_id", user.id);
+
+ if (error) throw error;
+
+ // Call success callback
+ onSuccess?.();
+ } catch (error) {
+ console.error("Error updating profile:", error);
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/sigap-website/app/_components/admin/settings/profile-settings.tsx b/sigap-website/app/_components/admin/settings/profile-settings.tsx
new file mode 100644
index 0000000..1552635
--- /dev/null
+++ b/sigap-website/app/_components/admin/settings/profile-settings.tsx
@@ -0,0 +1,284 @@
+"use client";
+
+import type React from "react";
+
+import type { User } from "@/src/models/users/users.model";
+
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Loader2, ImageIcon } from "lucide-react";
+
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/app/_components/ui/form";
+import { Input } from "@/app/_components/ui/input";
+import { Button } from "@/app/_components/ui/button";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/app/_components/ui/avatar";
+import { Label } from "@/app/_components/ui/label";
+import { Separator } from "@/app/_components/ui/separator";
+import { Switch } from "@/app/_components/ui/switch";
+import { useRef, useState } from "react";
+import { createClient } from "@/utils/supabase/client";
+import { ScrollArea } from "@/app/_components/ui/scroll-area";
+
+const profileFormSchema = z.object({
+ preferred_name: z.string().nullable().optional(),
+ avatar: z.string().nullable().optional(),
+});
+
+type ProfileFormValues = z.infer;
+
+interface ProfileSettingsProps {
+ user: User | null;
+}
+
+export function ProfileSettings({ user }: ProfileSettingsProps) {
+ const [isUploading, setIsUploading] = useState(false);
+ const fileInputRef = useRef(null);
+ const supabase = createClient();
+
+ // Use profile data with fallbacks
+ const preferredName = user?.profile?.first_name || "";
+ const userEmail = user?.email || "";
+ const userAvatar = user?.profile?.avatar || "";
+
+ const form = useForm({
+ resolver: zodResolver(profileFormSchema),
+ defaultValues: {
+ preferred_name: preferredName || "",
+ avatar: userAvatar || "",
+ },
+ });
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !user?.id) return;
+
+ try {
+ setIsUploading(true);
+
+ // Upload to Supabase Storage
+ const fileExt = file.name.split(".").pop();
+ const fileName = `${user.id}-${Date.now()}.${fileExt}`;
+ const filePath = `avatars/${fileName}`;
+
+ const { error: uploadError, data } = await supabase.storage
+ .from("profiles")
+ .upload(filePath, file, {
+ upsert: true,
+ contentType: file.type,
+ });
+
+ if (uploadError) throw uploadError;
+
+ // Get the public URL
+ const {
+ data: { publicUrl },
+ } = supabase.storage.from("profiles").getPublicUrl(filePath);
+
+ // Update the form value
+ form.setValue("avatar", publicUrl);
+ } catch (error) {
+ console.error("Error uploading avatar:", error);
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handleAvatarClick = () => {
+ fileInputRef.current?.click();
+ };
+
+ async function onSubmit(data: ProfileFormValues) {
+ try {
+ if (!user?.id) return;
+
+ // Update profile in database
+ const { error } = await supabase
+ .from("profiles")
+ .update({
+ first_name: data.preferred_name,
+ avatar: data.avatar,
+ })
+ .eq("user_id", user.id);
+
+ if (error) throw error;
+ } catch (error) {
+ console.error("Error updating profile:", error);
+ }
+ }
+
+ return (
+
+
+
+
+
+
Account
+
+
+
+
+
+
+ {preferredName?.[0]?.toUpperCase() ||
+ userEmail?.[0]?.toUpperCase()}
+
+
+ {isUploading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Preferred name
+ (
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ {/*
+ {form.formState.isSubmitting ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ "Save changes"
+ )}
+ */}
+
+
+
+
+
Account security
+
+
+
+
+
+
+
Password
+
+ Set a permanent password to login to your account.
+
+
+
+ Change password
+
+
+
+
+
+
2-step verification
+
+ Add an additional layer of security to your account during
+ login.
+
+
+
+ Add verification method
+
+
+
+
+
+
Passkeys
+
+ Securely sign-in with on-device biometric authentication.
+
+
+
+ Add passkey
+
+
+
+
+
+
+
Support
+
+
+
+
+
Support access
+
+ Grant temporary access to your account for support purposes.
+
+
+
+
+
+
+
+
Delete account
+
+ Permanently delete the account and remove access from all
+ workspaces.
+
+
+
+ Delete account
+
+
+
+
+
+
+ );
+}
diff --git a/sigap-website/app/_components/admin/settings/security-setting.tsx b/sigap-website/app/_components/admin/settings/security-setting.tsx
new file mode 100644
index 0000000..b86bc98
--- /dev/null
+++ b/sigap-website/app/_components/admin/settings/security-setting.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import type { User } from "@/src/models/users/users.model";
+import { Button } from "@/app/_components/ui/button";
+import { Separator } from "@/app/_components/ui/separator";
+
+interface SecuritySettingsProps {
+ user: User | null;
+}
+
+export function SecuritySettings({ user }: SecuritySettingsProps) {
+ return (
+
+
+
Account Security
+
+ Manage your account security settings
+
+
+
+
+
+
+
+
+
Email
+
{user?.email}
+
+
Change email
+
+
+
+
+
+
+
Password
+
+ Set a permanent password to login to your account.
+
+
+
Change password
+
+
+
+
+
+
+
Two-step verification
+
+ Add an additional layer of security to your account during login.
+
+
+
Add verification method
+
+
+
+
+
+
+
Passkeys
+
+ Securely sign-in with on-device biometric authentication.
+
+
+
Add passkey
+
+
+
+ );
+}
diff --git a/sigap-website/app/_components/admin/settings/setting-dialog.tsx b/sigap-website/app/_components/admin/settings/setting-dialog.tsx
new file mode 100644
index 0000000..ce8a9ba
--- /dev/null
+++ b/sigap-website/app/_components/admin/settings/setting-dialog.tsx
@@ -0,0 +1,191 @@
+"use client";
+
+import * as React from "react";
+import { cn } from "@/lib/utils";
+import {
+ Dialog,
+ DialogContent,
+ DialogTrigger,
+} from "@/app/_components/ui/dialog";
+import { ScrollArea } from "@/app/_components/ui/scroll-area";
+import { Separator } from "@/app/_components/ui/separator";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/app/_components/ui/avatar";
+import {
+ IconBell,
+ IconFingerprint,
+ IconLock,
+ IconPlugConnected,
+ IconSettings,
+ IconUser,
+ IconUsers,
+ IconWorld,
+} from "@tabler/icons-react";
+import type { User } from "@/src/models/users/users.model";
+import { ProfileSettings } from "./profile-settings";
+import { DialogTitle } from "@radix-ui/react-dialog";
+
+interface SettingsDialogProps {
+ user: User | null;
+ trigger: React.ReactNode;
+ defaultTab?: string;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+interface SettingsTab {
+ id: string;
+ icon: typeof IconUser;
+ title: string;
+ content: React.ReactNode;
+}
+
+interface SettingsSection {
+ title: string;
+ tabs: SettingsTab[];
+}
+
+export function SettingsDialog({
+ user,
+ trigger,
+ defaultTab = "account",
+ open,
+ onOpenChange,
+}: SettingsDialogProps) {
+ const [selectedTab, setSelectedTab] = React.useState(defaultTab);
+
+ // Get user display name
+ const preferredName = user?.profile?.first_name || "";
+ const userEmail = user?.email || "";
+ const displayName = preferredName || userEmail?.split("@")[0] || "User";
+ const userAvatar = user?.profile?.avatar || "";
+
+ const sections: SettingsSection[] = [
+ {
+ title: "Account",
+ tabs: [
+ {
+ id: "account",
+ icon: IconUser,
+ title: "My Account",
+ content: ,
+ },
+ {
+ id: "preferences",
+ icon: IconSettings,
+ title: "Preferences",
+ content: Preferences content
,
+ },
+ {
+ id: "notifications",
+ icon: IconBell,
+ title: "Notifications",
+ content: Notifications content
,
+ },
+ {
+ id: "connections",
+ icon: IconPlugConnected,
+ title: "Connections",
+ content: Connections content
,
+ },
+ ],
+ },
+ {
+ title: "Workspace",
+ tabs: [
+ {
+ id: "general",
+ icon: IconWorld,
+ title: "General",
+ content: General 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
,
+ },
+ ],
+ },
+ ];
+
+ const currentTab = sections
+ .flatMap((section) => section.tabs)
+ .find((tab) => tab.id === selectedTab);
+
+ return (
+
+
+ {trigger}
+
+
+ {/* Sidebar */}
+
+
+
+
+
+
+
+ {displayName[0].toUpperCase()}
+
+
+
{displayName}
+
+ {sections.map((section, index) => (
+
+
+
+ {section.title}
+
+
+
+ {section.tabs.map((tab) => (
+ setSelectedTab(tab.id)}
+ className={cn(
+ "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium",
+ tab.id === selectedTab
+ ? "bg-accent text-accent-foreground"
+ : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
+ )}
+ >
+
+ {tab.title}
+
+ ))}
+
+ {index < sections.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+ {/* Content */}
+
+
{currentTab?.content}
+
+
+
+
+ );
+}
diff --git a/sigap-website/components/admin/users/add-user-dialog.tsx b/sigap-website/app/_components/admin/users/add-user-dialog.tsx
similarity index 94%
rename from sigap-website/components/admin/users/add-user-dialog.tsx
rename to sigap-website/app/_components/admin/users/add-user-dialog.tsx
index 5526411..3a5865b 100644
--- a/sigap-website/components/admin/users/add-user-dialog.tsx
+++ b/sigap-website/app/_components/admin/users/add-user-dialog.tsx
@@ -8,11 +8,11 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Checkbox } from "@/components/ui/checkbox";
-import { createUser } from "@/app/protected/(admin)/dashboard/user-management/action";
+} from "@/app/_components/ui/dialog";
+import { Button } from "@/app/_components/ui/button";
+import { Input } from "@/app/_components/ui/input";
+import { Checkbox } from "@/app/_components/ui/checkbox";
+import { createUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { toast } from "sonner";
import { Mail, Lock, Loader2, X } from "lucide-react";
diff --git a/sigap-website/components/admin/users/column.tsx b/sigap-website/app/_components/admin/users/column.tsx
similarity index 63%
rename from sigap-website/components/admin/users/column.tsx
rename to sigap-website/app/_components/admin/users/column.tsx
index e3ce86d..a1ddb50 100644
--- a/sigap-website/components/admin/users/column.tsx
+++ b/sigap-website/app/_components/admin/users/column.tsx
@@ -1,10 +1,10 @@
-"use client"
+"use client";
-import type { ColumnDef } from "@tanstack/react-table"
-import { Badge } from "@/components/ui/badge"
-import { Checkbox } from "@/components/ui/checkbox"
-import { MoreHorizontal } from "lucide-react"
-import { Button } from "@/components/ui/button"
+import type { ColumnDef } from "@tanstack/react-table";
+import { Badge } from "@/app/_components/ui/badge";
+import { Checkbox } from "@/app/_components/ui/checkbox";
+import { MoreHorizontal } from "lucide-react";
+import { Button } from "@/app/_components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -12,29 +12,31 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { formatDate } from "date-fns"
-
+} from "@/app/_components/ui/dropdown-menu";
+import { formatDate } from "date-fns";
export type User = {
- id: string
- email: string
- first_name: string | null
- last_name: string | null
- role: string
- created_at: string
- last_sign_in_at: string | null
- email_confirmed_at: string | null
- is_anonymous: boolean
- banned_until: string | null
-}
+ id: string;
+ email: string;
+ first_name: string | null;
+ last_name: string | null;
+ role: string;
+ created_at: string;
+ last_sign_in_at: string | null;
+ email_confirmed_at: string | null;
+ is_anonymous: boolean;
+ banned_until: string | null;
+};
export const columns: ColumnDef[] = [
{
id: "select",
header: ({ table }) => (
table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
@@ -53,7 +55,9 @@ export const columns: ColumnDef[] = [
{
accessorKey: "email",
header: "Email",
- cell: ({ row }) => {row.getValue("email")}
,
+ cell: ({ row }) => (
+ {row.getValue("email")}
+ ),
},
{
accessorKey: "first_name",
@@ -77,20 +81,28 @@ export const columns: ColumnDef[] = [
{
accessorKey: "created_at",
header: "Created At",
- cell: ({ row }) => {formatDate(new Date(row.getValue("created_at")), "yyyy-MM-dd")}
,
+ cell: ({ row }) => (
+
+ {formatDate(new Date(row.getValue("created_at")), "yyyy-MM-dd")}
+
+ ),
},
{
accessorKey: "email_confirmed_at",
header: "Email Verified",
cell: ({ row }) => {
- const verified = row.getValue("email_confirmed_at") !== null
- return {verified ? "Verified" : "Unverified"}
+ const verified = row.getValue("email_confirmed_at") !== null;
+ return (
+
+ {verified ? "Verified" : "Unverified"}
+
+ );
},
},
{
id: "actions",
cell: ({ row }) => {
- const user = row.original
+ const user = row.original;
return (
@@ -107,11 +119,12 @@ export const columns: ColumnDef[] = [
Reset password
Send magic link
- Delete user
+
+ Delete user
+
- )
+ );
},
},
-]
-
+];
diff --git a/sigap-website/components/admin/users/data-table.tsx b/sigap-website/app/_components/admin/users/data-table.tsx
similarity index 97%
rename from sigap-website/components/admin/users/data-table.tsx
rename to sigap-website/app/_components/admin/users/data-table.tsx
index 688bd85..871636f 100644
--- a/sigap-website/components/admin/users/data-table.tsx
+++ b/sigap-website/app/_components/admin/users/data-table.tsx
@@ -19,14 +19,14 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
+} from "@/app/_components/ui/table";
+import { Button } from "@/app/_components/ui/button";
+import { Input } from "@/app/_components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
+} from "@/app/_components/ui/dropdown-menu";
import {
ChevronLeft,
ChevronRight,
@@ -34,14 +34,14 @@ import {
ChevronsRight,
Filter,
} from "lucide-react";
-import { Skeleton } from "@/components/ui/skeleton";
+import { Skeleton } from "@/app/_components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select";
+} from "@/app/_components/ui/select";
interface DataTableProps {
columns: ColumnDef[];
diff --git a/sigap-website/components/admin/users/invite-user.tsx b/sigap-website/app/_components/admin/users/invite-user.tsx
similarity index 88%
rename from sigap-website/components/admin/users/invite-user.tsx
rename to sigap-website/app/_components/admin/users/invite-user.tsx
index c301432..df39963 100644
--- a/sigap-website/components/admin/users/invite-user.tsx
+++ b/sigap-website/app/_components/admin/users/invite-user.tsx
@@ -10,13 +10,13 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
+} from "@/app/_components/ui/dialog";
+import { Button } from "@/app/_components/ui/button";
+import { Label } from "@/app/_components/ui/label";
+import { Input } from "@/app/_components/ui/input";
+import { Textarea } from "@/app/_components/ui/textarea";
import { useMutation } from "@tanstack/react-query";
-import { inviteUser } from "@/app/protected/(admin)/dashboard/user-management/action";
+import { inviteUser } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { toast } from "sonner";
interface InviteUserDialogProps {
@@ -60,7 +60,6 @@ export function InviteUserDialog({
try {
await inviteUser({
email: formData.email,
- user_metadata: metadata,
});
toast.success("Invitation sent");
onUserInvited();
diff --git a/sigap-website/components/admin/users/sheet.tsx b/sigap-website/app/_components/admin/users/sheet.tsx
similarity index 97%
rename from sigap-website/components/admin/users/sheet.tsx
rename to sigap-website/app/_components/admin/users/sheet.tsx
index 8226af2..d34eb6b 100644
--- a/sigap-website/components/admin/users/sheet.tsx
+++ b/sigap-website/app/_components/admin/users/sheet.tsx
@@ -9,10 +9,10 @@ import {
SheetFooter,
SheetHeader,
SheetTitle,
-} from "@/components/ui/sheet";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Separator } from "@/components/ui/separator";
+} from "@/app/_components/ui/sheet";
+import { Button } from "@/app/_components/ui/button";
+import { Badge } from "@/app/_components/ui/badge";
+import { Separator } from "@/app/_components/ui/separator";
import {
AlertDialog,
AlertDialogAction,
@@ -23,7 +23,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
-} from "@/components/ui/alert-dialog";
+} from "@/app/_components/ui/alert-dialog";
import {
Mail,
Trash2,
@@ -40,7 +40,7 @@ import {
sendMagicLink,
sendPasswordRecovery,
unbanUser,
-} from "@/app/protected/(admin)/dashboard/user-management/action";
+} from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { format } from "date-fns";
interface UserDetailsSheetProps {
diff --git a/sigap-website/components/admin/users/user-form.tsx b/sigap-website/app/_components/admin/users/user-form.tsx
similarity index 96%
rename from sigap-website/components/admin/users/user-form.tsx
rename to sigap-website/app/_components/admin/users/user-form.tsx
index 0b11988..911336e 100644
--- a/sigap-website/components/admin/users/user-form.tsx
+++ b/sigap-website/app/_components/admin/users/user-form.tsx
@@ -4,10 +4,10 @@
// import { useForm } from "react-hook-form"
// import { z } from "zod"
-// import { Button } from "@/components/ui/button"
-// import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
-// import { Input } from "@/components/ui/input"
-// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+// import { Button } from "@/app/_components/ui/button"
+// import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/app/_components/ui/form"
+// import { Input } from "@/app/_components/ui/input"
+// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
// import { useState } from "react"
// import { User } from "./column"
// import { updateUser } from "../../user-management/action"
@@ -145,4 +145,3 @@
//
// )
// }
-
diff --git a/sigap-website/components/admin/users/user-management.tsx b/sigap-website/app/_components/admin/users/user-management.tsx
similarity index 96%
rename from sigap-website/components/admin/users/user-management.tsx
rename to sigap-website/app/_components/admin/users/user-management.tsx
index cbc820a..5b3dfe0 100644
--- a/sigap-website/components/admin/users/user-management.tsx
+++ b/sigap-website/app/_components/admin/users/user-management.tsx
@@ -19,9 +19,9 @@ import {
ShieldAlert,
ListFilter,
} from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Badge } from "@/components/ui/badge";
+import { Button } from "@/app/_components/ui/button";
+import { Input } from "@/app/_components/ui/input";
+import { Badge } from "@/app/_components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
@@ -29,9 +29,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
-} from "@/components/ui/dropdown-menu";
+} from "@/app/_components/ui/dropdown-menu";
import { useQuery } from "@tanstack/react-query";
-import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action";
+import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
import { User } from "@/src/models/users/users.model";
import { toast } from "sonner";
import { DataTable } from "./data-table";
@@ -166,17 +166,25 @@ export default function UserManagement() {
if (filters.createdAt === "today") {
const today = new Date();
today.setHours(0, 0, 0, 0);
- const createdAt = new Date(user.created_at);
+ const createdAt = user.created_at
+ ? user.created_at
+ ? new Date(user.created_at)
+ : new Date()
+ : new Date();
if (createdAt < today) return false;
} else if (filters.createdAt === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
- const createdAt = new Date(user.created_at);
+ const createdAt = user.created_at
+ ? new Date(user.created_at)
+ : new Date();
if (createdAt < weekAgo) return false;
} else if (filters.createdAt === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
- const createdAt = new Date(user.created_at);
+ const createdAt = user.created_at
+ ? new Date(user.created_at)
+ : new Date();
if (createdAt < monthAgo) return false;
}
}
@@ -435,7 +443,9 @@ export default function UserManagement() {
),
cell: ({ row }: { row: { original: User } }) => {
- return new Date(row.original.created_at).toLocaleString();
+ return row.original.created_at
+ ? new Date(row.original.created_at).toLocaleString()
+ : "N/A";
},
},
{
diff --git a/sigap-website/components/admin/users/user-stats.tsx b/sigap-website/app/_components/admin/users/user-stats.tsx
similarity index 68%
rename from sigap-website/components/admin/users/user-stats.tsx
rename to sigap-website/app/_components/admin/users/user-stats.tsx
index 61d644c..3f0b4d5 100644
--- a/sigap-website/components/admin/users/user-stats.tsx
+++ b/sigap-website/app/_components/admin/users/user-stats.tsx
@@ -1,33 +1,36 @@
-"use client"
-
-import { useQuery } from "@tanstack/react-query"
-import { Card, CardContent } from "@/components/ui/card"
-import { Users, UserCheck, UserX } from "lucide-react"
-import { fetchUsers } from "@/app/protected/(admin)/dashboard/user-management/action"
-import { User } from "@/src/models/users/users.model"
+"use client";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent } from "@/app/_components/ui/card";
+import { Users, UserCheck, UserX } from "lucide-react";
+import { fetchUsers } from "@/app/(protected)/(admin)/dashboard/user-management/action";
+import { User } from "@/src/models/users/users.model";
function calculateUserStats(users: User[]) {
- const totalUsers = users.length
- const activeUsers = users.filter((user) => !user.banned_until && user.email_confirmed_at).length
- const inactiveUsers = totalUsers - activeUsers
+ const totalUsers = users.length;
+ const activeUsers = users.filter(
+ (user) => !user.banned_until && user.email_confirmed_at
+ ).length;
+ const inactiveUsers = totalUsers - activeUsers;
return {
totalUsers,
activeUsers,
inactiveUsers,
- activePercentage: totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
- inactivePercentage: totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
- }
+ activePercentage:
+ totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
+ inactivePercentage:
+ totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
+ };
}
export function UserStats() {
const { data: users = [], isLoading } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
- })
+ });
- const stats = calculateUserStats(users)
+ const stats = calculateUserStats(users);
if (isLoading) {
return (
@@ -44,7 +47,7 @@ export function UserStats() {
))}
>
- )
+ );
}
const cards = [
@@ -66,7 +69,7 @@ export function UserStats() {
subtitle: `${stats.inactivePercentage}% of total users`,
icon: UserX,
},
- ]
+ ];
return (
<>
@@ -74,7 +77,9 @@ export function UserStats() {