add trigger for auth.users to custom public.user

This commit is contained in:
vergiLgood1 2025-02-27 19:58:21 +07:00
parent 39a0af5e0f
commit 10ce404a1d
32 changed files with 1791 additions and 620 deletions

View File

@ -1,3 +1,5 @@
{
"recommendations": ["denoland.vscode-deno"]
"recommendations": [
"denoland.vscode-deno"
]
}

12
.vscode/settings.json vendored
View File

@ -1,5 +1,10 @@
{
"deno.enablePaths": ["supabase/functions"],
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"deno.enablePaths": [
"supabase/functions"
],
"deno.lint": true,
"deno.unstable": [
"bare-node-builtins",
@ -15,8 +20,5 @@
"fs",
"http",
"net"
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
]
}

View File

@ -0,0 +1,76 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/app/_components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
),
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -15,7 +15,7 @@ import {
SidebarRail,
} from "@/app/_components/ui/sidebar";
import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/data/nav";
import { navData } from "@/prisma/data/nav";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (

View File

@ -0,0 +1,235 @@
"use client";
import { useState } from "react";
import { Button } from "@/app/_components/ui/button";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/app/_components/ui/sheet";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { User } from "./users-table";
interface AddUserSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (
userData: Omit<User, "id" | "lastSignedIn"> & { password: string }
) => void;
}
export function AddUserSheet({
open,
onOpenChange,
onSave,
}: AddUserSheetProps) {
const [userData, setUserData] = useState<
Omit<User, "id" | "lastSignedIn"> & { password: string }
>({
email: "",
firstName: "",
lastName: "",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "active",
password: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!userData.email) {
newErrors.email = "Email is required";
} else if (!/\S+@\S+\.\S+/.test(userData.email)) {
newErrors.email = "Email is invalid";
}
if (!userData.password) {
newErrors.password = "Password is required";
} else if (userData.password.length < 6) {
newErrors.password = "Password must be at least 6 characters";
}
if (!userData.firstName) {
newErrors.firstName = "First name is required";
}
if (!userData.lastName) {
newErrors.lastName = "Last name is required";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = () => {
if (validateForm()) {
onSave(userData);
// Reset form
setUserData({
email: "",
firstName: "",
lastName: "",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "active",
password: "",
});
setErrors({});
}
};
const handleInputChange = (field: keyof typeof userData, value: string) => {
setUserData((prev) => ({ ...prev, [field]: value }));
// Clear error when field is edited
if (errors[field]) {
setErrors((prev) => {
const newErrors = { ...prev };
delete newErrors[field];
return newErrors;
});
}
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md">
<SheetHeader>
<SheetTitle>Add New User</SheetTitle>
<SheetDescription>
Create a new user account with Supabase authentication.
</SheetDescription>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input
id="email"
value={userData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
className="col-span-3"
/>
{errors.email && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{errors.email}
</p>
)}
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="password" className="text-right">
Password
</Label>
<Input
id="password"
type="password"
value={userData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
className="col-span-3"
/>
{errors.password && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{errors.password}
</p>
)}
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="firstName" className="text-right">
First Name
</Label>
<Input
id="firstName"
value={userData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)}
className="col-span-3"
/>
{errors.firstName && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{errors.firstName}
</p>
)}
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="lastName" className="text-right">
Last Name
</Label>
<Input
id="lastName"
value={userData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)}
className="col-span-3"
/>
{errors.lastName && (
<p className="col-span-3 col-start-2 text-sm text-red-500">
{errors.lastName}
</p>
)}
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="role" className="text-right">
Role
</Label>
<Select
value={userData.role}
onValueChange={(value: "admin" | "staff" | "user") =>
handleInputChange("role", value)
}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="staff">Staff</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="status" className="text-right">
Status
</Label>
<Select
value={userData.status}
onValueChange={(value: "active" | "inactive") =>
handleInputChange("status", value)
}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select a status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<SheetFooter>
<Button type="submit" onClick={handleSubmit}>
Add User
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,232 @@
"use client";
import { useMemo } from "react";
import { type ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, Filter, MoreVertical } from "lucide-react";
import { Button } from "@/app/_components/ui/button";
import { Checkbox } from "@/app/_components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/app/_components/ui/dropdown-menu";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
import { User } from "@/types/user";
interface ColumnOptions {
openEditSheet: (user: User) => void;
handleRoleFilter: (role: string) => void;
handleStatusFilter: (status: string) => void;
}
export const useColumns = ({
openEditSheet,
handleRoleFilter,
handleStatusFilter,
}: ColumnOptions) => {
// Use useMemo to prevent unnecessary re-creation of columns array
const columns = useMemo<ColumnDef<User>[]>(
() => [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "usersColoumn",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
Users
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const user = row.original;
return (
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage
src={user.avatar}
alt={`${user.firstName} ${user.lastName}`}
/>
<AvatarFallback>
{user.firstName[0]}
{user.lastName[0]}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">
{user.firstName} {user.lastName}
</div>
<div className="text-sm text-muted-foreground">
{user.email}
</div>
</div>
</div>
);
},
filterFn: (row, id, filterValue) => {
const searchTerm = filterValue.toLowerCase();
const user = row.original;
return (
user.firstName.toLowerCase().includes(searchTerm) ||
user.lastName.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm)
);
},
},
{
accessorKey: "role",
header: ({ column }) => {
return (
<div className="flex items-center space-x-2">
<span>Role</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Filter className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter by Role</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleRoleFilter("all")}>
All
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRoleFilter("admin")}>
Admin
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRoleFilter("staff")}>
Staff
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRoleFilter("user")}>
User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
},
{
accessorKey: "status",
header: ({ column }) => {
return (
<div className="flex items-center space-x-2">
<span>Status</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<Filter className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleStatusFilter("all")}>
All
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleStatusFilter("active")}
>
Active
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleStatusFilter("inactive")}
>
Inactive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
},
cell: ({ row }) => {
const status = row.getValue("status") as string;
return (
<div
className={`capitalize ${status === "active" ? "text-green-600" : "text-red-600"}`}
>
{status}
</div>
);
},
},
{
accessorKey: "lastSignedIn",
header: "Last Sign In",
},
{
id: "actions",
cell: ({ row }) => {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(user.id)}
>
Copy user ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => openEditSheet(user)}>
Edit user
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">
Delete user
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
],
[openEditSheet, handleRoleFilter, handleStatusFilter]
);
return columns;
};

View File

@ -1,14 +1,32 @@
"use client";
import { Button } from "@/app/_components/ui/button";
import { useState, useEffect } from "react";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { CalendarIcon, X } from "lucide-react";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetFooter,
SheetClose,
} from "@/app/_components/ui/sheet";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/app/_components/ui/form";
import { Input } from "@/app/_components/ui/input";
import { Button } from "@/app/_components/ui/button";
import {
Select,
SelectContent,
@ -16,94 +34,210 @@ import {
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { Textarea } from "@/app/_components/ui/textarea";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/app/_components/ui/sheet";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
import { Calendar } from "@/app/_components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/app/_components/ui/popover";
import { Separator } from "@/app/_components/ui/separator";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/app/_components/ui/tabs";
import { User } from "./users-table";
import type { User } from "./users-table";
import { useCallback } from "react";
const formSchema = z.object({
firstName: z.string().min(2, {
message: "First name must be at least 2 characters.",
}),
lastName: z.string().min(2, {
message: "Last name must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
// Create a schema for form validation
const userFormSchema = z.object({
// User fields
email: z.string().email({ message: "Please enter a valid email address" }),
firstName: z.string().min(1, { message: "First name is required" }),
lastName: z.string().min(1, { message: "Last name is required" }),
avatar: z.string().optional(),
role: z.enum(["admin", "staff", "user"]),
status: z.enum(["active", "inactive"]),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters long" })
.optional(),
// Profile fields
bio: z.string().optional(),
phone: z.string().optional(),
address: z.string().optional(),
city: z.string().optional(),
country: z.string().optional(),
birthDate: z.date().optional(),
});
type UserFormValues = z.infer<typeof userFormSchema>;
// Extended user type to include profile information
type UserWithProfile = User & {
profile?: {
bio?: string;
phone?: string;
address?: string;
city?: string;
country?: string;
birthDate?: Date;
};
};
interface EditUserSheetProps {
isOpen: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
selectedUser: User | null;
onUserUpdate: (updatedUser: User) => void;
user?: UserWithProfile | null;
onSave: (user: UserWithProfile) => void;
isNew?: boolean;
}
export function EditUserSheet({
isOpen,
open,
onOpenChange,
selectedUser,
onUserUpdate,
user,
onSave,
isNew = false,
}: EditUserSheetProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
const form = useForm<UserFormValues>({
resolver: zodResolver(userFormSchema),
defaultValues: {
firstName: selectedUser?.firstName || "",
lastName: selectedUser?.lastName || "",
email: selectedUser?.email || "",
role: selectedUser?.role || "user",
status: selectedUser?.status || "inactive",
email: "",
firstName: "",
lastName: "",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
password: isNew ? "" : undefined,
bio: "",
phone: "",
address: "",
city: "",
country: "",
birthDate: undefined,
},
});
const onSubmit = useCallback(
(values: z.infer<typeof formSchema>) => {
if (!selectedUser) return;
// Update form when user changes
useEffect(() => {
if (user) {
const birthDate = user.profile?.birthDate
? new Date(user.profile.birthDate)
: undefined;
const updatedUser: User = {
...selectedUser,
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
role: values.role,
status: values.status,
form.reset({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
avatar: user.avatar,
role: user.role,
// Don't populate password for existing users
bio: user.profile?.bio || "",
phone: user.profile?.phone || "",
address: user.profile?.address || "",
city: user.profile?.city || "",
country: user.profile?.country || "",
birthDate: birthDate,
});
} else if (isNew) {
form.reset({
email: "",
firstName: "",
lastName: "",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
password: "",
bio: "",
phone: "",
address: "",
city: "",
country: "",
birthDate: undefined,
});
}
}, [user, isNew, form]);
const onSubmit = (data: UserFormValues) => {
// Prepare the user object with profile
const updatedUser: UserWithProfile = {
id: user?.id || "new-id", // In a real app, this would be handled by the backend
email: data.email,
firstName: data.firstName || "",
lastName: data.lastName || "",
avatar: data.avatar || "/placeholder.svg?height=40&width=40",
role: data.role,
status: user?.status || "active",
lastSignedIn:
user?.lastSignedIn || new Date().toISOString().split("T")[0],
profile: {
bio: data.bio,
phone: data.phone,
address: data.address,
city: data.city,
country: data.country,
birthDate: data.birthDate,
},
};
onUserUpdate(updatedUser);
// Save the user
onSave(updatedUser);
// Close the sheet
onOpenChange(false);
},
[selectedUser, onUserUpdate, onOpenChange]
);
};
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent side="right">
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-xl overflow-y-auto">
<SheetHeader>
<SheetTitle>Edit User</SheetTitle>
<SheetTitle>{isNew ? "Add New User" : "Edit User"}</SheetTitle>
<SheetDescription>
Make changes to the user profile here. Click save when you're done.
{isNew
? "Fill in the details to create a new user account."
: "Make changes to the user profile here."}
</SheetDescription>
</SheetHeader>
<div className="py-4">
<Tabs defaultValue="user" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="user">User Details</TabsTrigger>
<TabsTrigger value="profile">Profile Information</TabsTrigger>
</TabsList>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 pt-6"
>
<TabsContent value="user" className="space-y-4">
<div className="flex justify-center mb-6">
<Avatar className="h-24 w-24">
<AvatarImage
src={
form.watch("avatar") ||
"/placeholder.svg?height=96&width=96"
}
alt="User avatar"
/>
<AvatarFallback>
{form.watch("firstName")?.charAt(0) || ""}
{form.watch("lastName")?.charAt(0) || ""}
</AvatarFallback>
</Avatar>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First name</FormLabel>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="First name" {...field} />
</FormControl>
@ -116,7 +250,7 @@ export function EditUserSheet({
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last name</FormLabel>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Last name" {...field} />
</FormControl>
@ -124,6 +258,8 @@ export function EditUserSheet({
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
@ -131,12 +267,40 @@ export function EditUserSheet({
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
<Input
placeholder="Email address"
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isNew && (
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Set password"
type="password"
{...field}
/>
</FormControl>
<FormDescription>
Minimum 8 characters
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="role"
@ -146,10 +310,13 @@ export function EditUserSheet({
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger className="w-[180px]">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="staff">Staff</SelectItem>
@ -160,31 +327,155 @@ export function EditUserSheet({
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
name="avatar"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
<FormLabel>Avatar URL</FormLabel>
<FormControl>
<Input placeholder="Avatar URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save changes</Button>
</TabsContent>
<TabsContent value="profile" className="space-y-4">
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little about this user"
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone</FormLabel>
<FormControl>
<Input placeholder="Phone number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Address</FormLabel>
<FormControl>
<Input placeholder="Street address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>City</FormLabel>
<FormControl>
<Input placeholder="City" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="country"
render={({ field }) => (
<FormItem>
<FormLabel>Country</FormLabel>
<FormControl>
<Input placeholder="Country" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="birthDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Date of Birth</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={`w-full pl-3 text-left font-normal ${
!field.value && "text-muted-foreground"
}`}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date: Date) =>
date > new Date() ||
date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<SheetFooter className="pt-4">
<SheetClose asChild>
<Button variant="outline" type="button">
Cancel
</Button>
</SheetClose>
<Button type="submit">
{isNew ? "Create User" : "Save Changes"}
</Button>
</SheetFooter>
</form>
</Form>
</Tabs>
</div>
</SheetContent>
</Sheet>
);

View File

@ -183,15 +183,15 @@ export function UsersTable() {
useEffect(() => {
if (searchValue) {
setColumnFilters((prev) => {
// Remove existing kolomusers filter if it exists
const filtered = prev.filter((filter) => filter.id !== "kolomusers");
// Remove existing usersColoumn filter if it exists
const filtered = prev.filter((filter) => filter.id !== "usersColoumn");
// Add the new search filter
return [...filtered, { id: "kolomusers", value: searchValue }];
return [...filtered, { id: "usersColoumn", value: searchValue }];
});
} else {
// Remove the filter if search is empty
setColumnFilters((prev) =>
prev.filter((filter) => filter.id !== "kolomusers")
prev.filter((filter) => filter.id !== "usersColoumn")
);
}
}, [searchValue]);
@ -256,7 +256,7 @@ export function UsersTable() {
enableHiding: false,
},
{
accessorKey: "kolomusers",
accessorKey: "usersColoumn",
header: ({ column }) => {
return (
<Button
@ -575,10 +575,10 @@ export function UsersTable() {
</div>
</div>
<EditUserSheet
isOpen={isSheetOpen}
onOpenChange={setIsSheetOpen}
selectedUser={selectedUser}
onUserUpdate={handleUserUpdate}
open={isSheetOpen}
onOpenChange={() => setIsSheetOpen(false)}
user={selectedUser}
onSave={handleUserUpdate}
/>
</div>
);

View File

@ -23,6 +23,7 @@
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-email/components": "0.0.33",
@ -33,7 +34,7 @@
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"framer-motion": "^12.4.7",
"input-otp": "^1.4.2",
"lucide-react": "^0.468.0",
@ -43,6 +44,7 @@
"next-themes": "^0.4.3",
"prettier": "^3.3.3",
"react": "18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.54.2",
"resend": "^4.1.2",
@ -2397,6 +2399,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-roving-focus": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
@ -3910,9 +3942,9 @@
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
@ -5863,6 +5895,20 @@
"node": ">=0.10.0"
}
},
"node_modules/react-day-picker": {
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
"integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
"license": "MIT",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"date-fns": "^2.28.0 || ^3.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -6906,9 +6952,9 @@
}
},
"node_modules/supabase": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.12.1.tgz",
"integrity": "sha512-vB6LX1KGrqku8AFlp2vJw49IUB9g6Rz2b84qpcWSZ3mMDFumA6hDSbXbFJUnr3hcvyPzoOsQlhMTZN7a6o3hfA==",
"version": "2.15.8",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.15.8.tgz",
"integrity": "sha512-yY4kVpdd7x9u5QqTW/8zUXIrMgdkBDGqQwkDugBLe8uoFdH9tVZKt0L5RmuM21RJ0MEQkby2sQrTfiXvgGyx9w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",

View File

@ -24,6 +24,7 @@
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-email/components": "0.0.33",
@ -34,7 +35,7 @@
"autoprefixer": "10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"framer-motion": "^12.4.7",
"input-otp": "^1.4.2",
"lucide-react": "^0.468.0",
@ -44,6 +45,7 @@
"next-themes": "^0.4.3",
"prettier": "^3.3.3",
"react": "18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.54.2",
"resend": "^4.1.2",

View File

View File

@ -1,43 +0,0 @@
-- CreateEnum
CREATE TYPE "roles" AS ENUM ('admin', 'staff', 'user');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"password" TEXT,
"firstName" TEXT,
"lastName" TEXT,
"avatar" TEXT,
"role" "roles" NOT NULL DEFAULT 'user',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"lastSignedIn" TIMESTAMP(3),
"metadata" JSONB,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "profiles" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"bio" TEXT,
"phone" TEXT,
"address" TEXT,
"city" TEXT,
"country" TEXT,
"birthDate" TIMESTAMP(3),
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "profiles_userId_key" ON "profiles"("userId");
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,18 +0,0 @@
-- CreateEnum
CREATE TYPE "status_contact_messages" AS ENUM ('new', 'read', 'replied', 'resolved');
-- CreateTable
CREATE TABLE "contact_messages" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT,
"phone" TEXT,
"message_type" TEXT,
"message_type_label" TEXT,
"message" TEXT,
"status" "status_contact_messages" NOT NULL DEFAULT 'new',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id")
);

View File

@ -1,106 +0,0 @@
/*
Warnings:
- You are about to drop the column `createdAt` on the `contact_messages` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `contact_messages` table. All the data in the column will be lost.
- You are about to drop the column `birthDate` on the `profiles` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `profiles` table. All the data in the column will be lost.
- You are about to drop the column `createdAt` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `emailVerified` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `firstName` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `lastName` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `lastSignedIn` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `updatedAt` on the `users` table. All the data in the column will be lost.
- A unique constraint covering the columns `[user_id]` on the table `profiles` will be added. If there are existing duplicate values, this will fail.
- Added the required column `user_id` to the `profiles` table without a default value. This is not possible if the table is not empty.
- Added the required column `updated_at` to the `users` table without a default value. This is not possible if the table is not empty.
*/
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- DropForeignKey
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_userId_fkey";
-- DropIndex
DROP INDEX "profiles_userId_key";
-- AlterTable
ALTER TABLE "contact_messages" DROP COLUMN "createdAt",
DROP COLUMN "updatedAt",
ADD COLUMN "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
ADD COLUMN "updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
ALTER COLUMN "id" SET DEFAULT gen_random_uuid();
-- AlterTable
ALTER TABLE "profiles" DROP COLUMN "birthDate",
DROP COLUMN "userId",
ADD COLUMN "birth_date" TIMESTAMP(3),
ADD COLUMN "user_id" TEXT NOT NULL,
ALTER COLUMN "id" SET DEFAULT gen_random_uuid();
-- AlterTable
ALTER TABLE "users" DROP COLUMN "createdAt",
DROP COLUMN "emailVerified",
DROP COLUMN "firstName",
DROP COLUMN "lastName",
DROP COLUMN "lastSignedIn",
DROP COLUMN "updatedAt",
ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "email_verified" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "first_name" TEXT,
ADD COLUMN "last_name" TEXT,
ADD COLUMN "last_signed_in" TIMESTAMP(3),
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL;
-- CreateTable
CREATE TABLE "nav_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(255) NOT NULL,
"icon" VARCHAR(100) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT false,
"order_seq" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"created_by" UUID,
"updated_by" UUID,
CONSTRAINT "nav_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sub_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(255) NOT NULL,
"order_seq" INTEGER NOT NULL,
"nav_item_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"created_by" UUID,
"updated_by" UUID,
CONSTRAINT "sub_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "nav_items_title_idx" ON "nav_items"("title");
-- CreateIndex
CREATE INDEX "nav_items_is_active_idx" ON "nav_items"("is_active");
-- CreateIndex
CREATE INDEX "sub_items_nav_item_id_idx" ON "sub_items"("nav_item_id");
-- CreateIndex
CREATE INDEX "sub_items_title_idx" ON "sub_items"("title");
-- CreateIndex
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sub_items" ADD CONSTRAINT "sub_items_nav_item_id_fkey" FOREIGN KEY ("nav_item_id") REFERENCES "nav_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,108 +0,0 @@
/*
Warnings:
- The primary key for the `contact_messages` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `contact_messages` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- You are about to alter the column `name` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `email` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `phone` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`.
- You are about to alter the column `message_type` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(50)`.
- You are about to alter the column `message_type_label` on the `contact_messages` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(50)`.
- The primary key for the `profiles` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `profiles` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- You are about to alter the column `phone` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`.
- You are about to alter the column `address` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `city` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
- You are about to alter the column `country` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
- The primary key for the `users` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to alter the column `email` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `password` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `avatar` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `first_name` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to alter the column `last_name` on the `users` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
- You are about to drop the `sub_items` table. If the table is not empty, all the data it contains will be lost.
- Changed the type of `user_id` on the `profiles` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `id` on the `users` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- DropForeignKey
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_user_id_fkey";
-- DropForeignKey
ALTER TABLE "sub_items" DROP CONSTRAINT "sub_items_nav_item_id_fkey";
-- AlterTable
ALTER TABLE "contact_messages" DROP CONSTRAINT "contact_messages_pkey",
DROP COLUMN "id",
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
ALTER COLUMN "name" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "email" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20),
ALTER COLUMN "message_type" SET DATA TYPE VARCHAR(50),
ALTER COLUMN "message_type_label" SET DATA TYPE VARCHAR(50),
ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now(),
ADD CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id");
-- AlterTable
ALTER TABLE "nav_items" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "profiles" DROP CONSTRAINT "profiles_pkey",
DROP COLUMN "id",
ADD COLUMN "id" UUID NOT NULL DEFAULT gen_random_uuid(),
ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20),
ALTER COLUMN "address" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "city" SET DATA TYPE VARCHAR(100),
ALTER COLUMN "country" SET DATA TYPE VARCHAR(100),
DROP COLUMN "user_id",
ADD COLUMN "user_id" UUID NOT NULL,
ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id");
-- AlterTable
ALTER TABLE "users" DROP CONSTRAINT "users_pkey",
DROP COLUMN "id",
ADD COLUMN "id" UUID NOT NULL,
ALTER COLUMN "email" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "password" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "avatar" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "first_name" SET DATA TYPE VARCHAR(255),
ALTER COLUMN "last_name" SET DATA TYPE VARCHAR(255),
ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id");
-- DropTable
DROP TABLE "sub_items";
-- CreateTable
CREATE TABLE "nav_sub_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(255) NOT NULL,
"order_seq" INTEGER NOT NULL,
"nav_item_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"created_by" UUID,
"updated_by" UUID,
CONSTRAINT "nav_sub_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "nav_sub_items_nav_item_id_idx" ON "nav_sub_items"("nav_item_id");
-- CreateIndex
CREATE INDEX "nav_sub_items_title_idx" ON "nav_sub_items"("title");
-- CreateIndex
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
-- CreateIndex
CREATE INDEX "users_role_idx" ON "users"("role");
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "nav_sub_items" ADD CONSTRAINT "nav_sub_items_nav_item_id_fkey" FOREIGN KEY ("nav_item_id") REFERENCES "nav_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,63 +0,0 @@
-- AlterTable
ALTER TABLE "cities" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_cases" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_categories" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "demographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "districts" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "geographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "nav_items" ALTER COLUMN "created_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "nav_sub_items" ALTER COLUMN "created_at" SET DEFAULT now();
-- CreateTable
CREATE TABLE "nav_sub_sub_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(255) NOT NULL,
"slug" VARCHAR(255) NOT NULL,
"icon" VARCHAR(100) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT false,
"order_seq" INTEGER NOT NULL,
"nav_sub_item_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ(6) NOT NULL,
"created_by" UUID,
"updated_by" UUID,
CONSTRAINT "nav_sub_sub_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "nav_sub_sub_items_nav_sub_item_id_idx" ON "nav_sub_sub_items"("nav_sub_item_id");
-- CreateIndex
CREATE INDEX "nav_sub_sub_items_title_idx" ON "nav_sub_sub_items"("title");
-- AddForeignKey
ALTER TABLE "nav_sub_sub_items" ADD CONSTRAINT "nav_sub_sub_items_nav_sub_item_id_fkey" FOREIGN KEY ("nav_sub_item_id") REFERENCES "nav_sub_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,32 +1,65 @@
/*
Warnings:
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
- Added the required column `slug` to the `nav_items` table without a default value. This is not possible if the table is not empty.
- Added the required column `icon` to the `nav_sub_items` table without a default value. This is not possible if the table is not empty.
- Added the required column `slug` to the `nav_sub_items` table without a default value. This is not possible if the table is not empty.
-- CreateEnum
CREATE TYPE "roles" AS ENUM ('admin', 'staff', 'user');
-- CreateEnum
CREATE TYPE "status_contact_messages" AS ENUM ('new', 'read', 'replied', 'resolved');
*/
-- CreateEnum
CREATE TYPE "crime_rates" AS ENUM ('low', 'medium', 'high');
-- CreateEnum
CREATE TYPE "crime_status" AS ENUM ('new', 'in_progress', 'resolved');
-- AlterTable
ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" DROP DEFAULT;
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"email" VARCHAR(255) NOT NULL,
"email_verified" BOOLEAN NOT NULL DEFAULT false,
"password" VARCHAR(255),
"first_name" VARCHAR(255),
"last_name" VARCHAR(255),
"avatar" VARCHAR(255),
"role" "roles" NOT NULL DEFAULT 'user',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"last_signed_in" TIMESTAMP(3),
"metadata" JSONB,
-- AlterTable
ALTER TABLE "nav_items" ADD COLUMN "slug" VARCHAR(255) NOT NULL,
ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" DROP DEFAULT;
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- AlterTable
ALTER TABLE "nav_sub_items" ADD COLUMN "icon" VARCHAR(100) NOT NULL,
ADD COLUMN "is_active" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "slug" VARCHAR(255) NOT NULL,
ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" DROP DEFAULT;
-- CreateTable
CREATE TABLE "profiles" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" UUID NOT NULL,
"bio" TEXT,
"phone" VARCHAR(20),
"address" VARCHAR(255),
"city" VARCHAR(100),
"country" VARCHAR(100),
"birth_date" TIMESTAMP(3),
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contact_messages" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" VARCHAR(255),
"email" VARCHAR(255),
"phone" VARCHAR(20),
"message_type" VARCHAR(50),
"message_type_label" VARCHAR(50),
"message" TEXT,
"status" "status_contact_messages" NOT NULL DEFAULT 'new',
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT now(),
"updated_at" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cities" (
@ -127,6 +160,18 @@ CREATE TABLE "crime_categories" (
CONSTRAINT "crime_categories_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_role_idx" ON "users"("role");
-- CreateIndex
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
-- CreateIndex
CREATE INDEX "profiles_user_id_idx" ON "profiles"("user_id");
-- CreateIndex
CREATE INDEX "cities_name_idx" ON "cities"("name");
@ -148,8 +193,8 @@ CREATE UNIQUE INDEX "crimes_district_id_year_key" ON "crimes"("district_id", "ye
-- CreateIndex
CREATE UNIQUE INDEX "crimes_city_id_year_key" ON "crimes"("city_id", "year");
-- CreateIndex
CREATE INDEX "profiles_user_id_idx" ON "profiles"("user_id");
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "cities" ADD CONSTRAINT "cities_geographic_id_fkey" FOREIGN KEY ("geographic_id") REFERENCES "geographics"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,64 @@
/*
Warnings:
- You are about to drop the column `last_signed_in` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `metadata` on the `users` table. All the data in the column will be lost.
- You are about to drop the column `password` on the `users` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "cities" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_cases" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_categories" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "demographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "districts" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "geographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "users" DROP COLUMN "last_signed_in",
DROP COLUMN "metadata",
DROP COLUMN "password",
ADD COLUMN "banned_until" TIMESTAMP(3),
ADD COLUMN "confirmation_sent_at" TIMESTAMP(3),
ADD COLUMN "confirmation_token" VARCHAR(255),
ADD COLUMN "deleted_at" TIMESTAMP(3),
ADD COLUMN "email_change" VARCHAR(255),
ADD COLUMN "email_change_sent_at" TIMESTAMP(3),
ADD COLUMN "email_change_token" VARCHAR(255),
ADD COLUMN "email_confirmed_at" TIMESTAMP(3),
ADD COLUMN "encrypted_password" VARCHAR(255),
ADD COLUMN "is_anonymous" BOOLEAN DEFAULT false,
ADD COLUMN "is_sso_user" BOOLEAN DEFAULT false,
ADD COLUMN "last_sign_in_at" TIMESTAMP(3),
ADD COLUMN "phone" VARCHAR(20),
ADD COLUMN "phone_confirmed_at" TIMESTAMP(3),
ADD COLUMN "providers" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "raw_app_meta_data" JSONB,
ADD COLUMN "raw_user_meta_data" JSONB,
ADD COLUMN "reauthentication_sent_at" TIMESTAMP(3),
ADD COLUMN "reauthentication_token" VARCHAR(255),
ADD COLUMN "recovery_sent_at" TIMESTAMP(3),
ADD COLUMN "recovery_token" VARCHAR(255);

View File

@ -0,0 +1,40 @@
/*
Warnings:
- The `providers` column on the `users` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "cities" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "contact_messages" ALTER COLUMN "created_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_cases" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crime_categories" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "crimes" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "demographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "districts" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "geographics" ALTER COLUMN "created_at" SET DEFAULT now(),
ALTER COLUMN "updated_at" SET DEFAULT now();
-- AlterTable
ALTER TABLE "users" DROP COLUMN "providers",
ADD COLUMN "providers" JSONB DEFAULT '[]';

View File

@ -20,15 +20,33 @@ model User {
id String @id @db.Uuid
email String @unique @db.VarChar(255)
emailVerified Boolean @default(false) @map("email_verified")
password String? @db.VarChar(255)
encryptedPassword String? @map("encrypted_password") @db.VarChar(255)
firstName String? @map("first_name") @db.VarChar(255)
lastName String? @map("last_name") @db.VarChar(255)
avatar String? @db.VarChar(255)
role Role @default(user)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastSignedIn DateTime? @map("last_signed_in")
metadata Json?
lastSignedIn DateTime? @map("last_sign_in_at")
phone String? @db.VarChar(20)
phoneConfirmedAt DateTime? @map("phone_confirmed_at")
emailConfirmedAt DateTime? @map("email_confirmed_at")
confirmationToken String? @map("confirmation_token") @db.VarChar(255)
confirmationSentAt DateTime? @map("confirmation_sent_at")
recoveryToken String? @map("recovery_token") @db.VarChar(255)
recoverySentAt DateTime? @map("recovery_sent_at")
emailChangeToken String? @map("email_change_token") @db.VarChar(255)
emailChange String? @map("email_change") @db.VarChar(255)
emailChangeSentAt DateTime? @map("email_change_sent_at")
reauthenticationToken String? @map("reauthentication_token") @db.VarChar(255)
reauthenticationSentAt DateTime? @map("reauthentication_sent_at")
isAnonymous Boolean? @default(false) @map("is_anonymous")
providers Json? @default("[]")
rawAppMetadata Json? @map("raw_app_meta_data")
rawUserMetadata Json? @map("raw_user_meta_data")
bannedUntil DateTime? @map("banned_until")
deletedAt DateTime? @map("deleted_at")
isSsoUser Boolean? @default(false) @map("is_sso_user")
profile Profile?

View File

@ -37,6 +37,14 @@ export const userSchema = z.object({
export type User = z.infer<typeof userSchema>;
export const userAuthSchema = userSchema.pick({
id: true,
email: true,
emailVerified: true,
role: true,
profile: true,
})
export const createUserSchema = userSchema
.pick({
id: true,

View File

@ -45,6 +45,11 @@ max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# Specifies an ordered list of schema files that describe your database.
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
@ -271,7 +276,7 @@ inspector_port = 8083
# static_files = [ "./functions/MY_FUNCTION_NAME/*.html" ]
[analytics]
enabled = true
enabled = false
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"

View File

@ -1,3 +0,0 @@
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries

View File

@ -1,3 +0,0 @@
{
"imports": {}
}

View File

@ -1,62 +0,0 @@
import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
import { Resend } from "npm:resend";
const resend = new Resend("re_4EyTgztQ_3istGdHQFeSTQsLBtH6oAdub" as string);
const hookSecret =
"jeroAB/CXdS721OiHV0Ac0yRcxO7eNihgjblH62xMhLBNc6OwK3DQnkbrHjTlSw5anml2onNTolG3SzZ" as string;
Deno.serve(async (req) => {
if (req.method !== "POST") {
return new Response("not allowed", { status: 400 });
}
const payload = await req.text();
const headers = Object.fromEntries(req.headers);
const wh = new Webhook(hookSecret);
try {
const { user, email_data } = wh.verify(payload, headers) as {
user: {
email: string;
};
email_data: {
token: string;
token_hash: string;
redirect_to: string;
email_action_type: string;
site_url: string;
token_new: string;
token_hash_new: string;
};
};
const { error } = await resend.emails.send({
from: "welcome <onboarding@example.com>",
to: [user.email],
subject: "Welcome to my site!",
text: `Confirm you signup with this code: ${email_data.token}`,
});
if (error) {
throw error;
}
} catch (error) {
return new Response(
JSON.stringify({
error: {
http_code: error.code,
message: error.message,
},
}),
{
status: 401,
headers: { "Content-Type": "application/json" },
}
);
}
const responseHeaders = new Headers();
responseHeaders.set("Content-Type", "application/json");
return new Response(JSON.stringify({}), {
status: 200,
headers: responseHeaders,
});
});

View File

@ -0,0 +1,212 @@
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
CREATE EXTENSION IF NOT EXISTS "pgsodium";
COMMENT ON SCHEMA "public" IS 'standard public schema';
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
CREATE TYPE "public"."crime_rates" AS ENUM (
'low',
'medium',
'high'
);
ALTER TYPE "public"."crime_rates" OWNER TO "prisma";
CREATE TYPE "public"."crime_status" AS ENUM (
'new',
'in_progress',
'resolved'
);
ALTER TYPE "public"."crime_status" OWNER TO "prisma";
CREATE TYPE "public"."roles" AS ENUM (
'admin',
'staff',
'user'
);
ALTER TYPE "public"."roles" OWNER TO "prisma";
CREATE TYPE "public"."status_contact_messages" AS ENUM (
'new',
'read',
'replied',
'resolved'
);
ALTER TYPE "public"."status_contact_messages" OWNER TO "prisma";
SET default_tablespace = '';
SET default_table_access_method = "heap";
CREATE TABLE IF NOT EXISTS "public"."_prisma_migrations" (
"id" character varying(36) NOT NULL,
"checksum" character varying(64) NOT NULL,
"finished_at" timestamp with time zone,
"migration_name" character varying(255) NOT NULL,
"logs" "text",
"rolled_back_at" timestamp with time zone,
"started_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"applied_steps_count" integer DEFAULT 0 NOT NULL
);
ALTER TABLE "public"."_prisma_migrations" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."cities" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"geographic_id" "uuid",
"name" character varying(100) NOT NULL,
"code" character varying(10) NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."cities" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."contact_messages" (
"name" character varying(255),
"email" character varying(255),
"phone" character varying(20),
"message_type" character varying(50),
"message_type_label" character varying(50),
"message" "text",
"status" "public"."status_contact_messages" DEFAULT 'new'::"public"."status_contact_messages" NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone NOT NULL,
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL
);
ALTER TABLE "public"."contact_messages" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."crime_cases" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"crime_id" "uuid",
"crime_category_id" "uuid",
"date" timestamp(6) with time zone NOT NULL,
"time" timestamp(6) with time zone NOT NULL,
"location" character varying(255) NOT NULL,
"latitude" double precision NOT NULL,
"longitude" double precision NOT NULL,
"description" "text" NOT NULL,
"victim_count" integer NOT NULL,
"status" "public"."crime_status" DEFAULT 'new'::"public"."crime_status" NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."crime_cases" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."crime_categories" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"name" character varying(255) NOT NULL,
"description" "text" NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."crime_categories" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."crimes" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"district_id" "uuid",
"city_id" "uuid",
"year" integer NOT NULL,
"number_of_crime" integer NOT NULL,
"rate" "public"."crime_rates" DEFAULT 'low'::"public"."crime_rates" NOT NULL,
"heat_map" "jsonb",
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."crimes" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."demographics" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"district_id" "uuid",
"city_id" "uuid",
"province_id" "uuid",
"year" integer NOT NULL,
"population" integer NOT NULL,
"population_density" double precision NOT NULL,
"poverty_rate" double precision NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."demographics" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."districts" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"city_id" "uuid" NOT NULL,
"name" character varying(100) NOT NULL,
"code" character varying(10) NOT NULL,
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."districts" OWNER TO "prisma";
CREATE TABLE IF NOT EXISTS "public"."geographics" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"district_id" "uuid",
"latitude" double precision,
"longitude" double precision,
"land_area" double precision,
"polygon" "jsonb",
"created_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp(6) with time zone DEFAULT "now"() NOT NULL
);
ALTER TABLE "public"."geographics" OWNER TO "prisma";
ALTER TABLE ONLY "public"."_prisma_migrations"
ADD CONSTRAINT "_prisma_migrations_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."cities"
ADD CONSTRAINT "cities_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."contact_messages"
ADD CONSTRAINT "contact_messages_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."crime_cases"
ADD CONSTRAINT "crime_cases_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."crime_categories"
ADD CONSTRAINT "crime_categories_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."crimes"
ADD CONSTRAINT "crimes_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."demographics"
ADD CONSTRAINT "demographics_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."districts"
ADD CONSTRAINT "districts_pkey" PRIMARY KEY ("id");
ALTER TABLE ONLY "public"."geographics"
ADD CONSTRAINT "geographics_pkey" PRIMARY KEY ("id");
CREATE INDEX "cities_name_idx" ON "public"."cities" USING "btree" ("name");
CREATE UNIQUE INDEX "crimes_city_id_year_key" ON "public"."crimes" USING "btree" ("city_id", "year");
CREATE UNIQUE INDEX "crimes_district_id_year_key" ON "public"."crimes" USING "btree" ("district_id", "year");
CREATE UNIQUE INDEX "demographics_city_id_year_key" ON "public"."demographics" USING "btree" ("city_id", "year");
CREATE UNIQUE INDEX "demographics_district_id_year_key" ON "public"."demographics" USING "btree" ("district_id", "year");
CREATE INDEX "districts_name_idx" ON "public"."districts" USING "btree" ("name");
CREATE UNIQUE INDEX "geographics_district_id_key" ON "public"."geographics" USING "btree" ("district_id");
ALTER TABLE ONLY "public"."cities"
ADD CONSTRAINT "cities_geographic_id_fkey" FOREIGN KEY ("geographic_id") REFERENCES "public"."geographics"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."crime_cases"
ADD CONSTRAINT "crime_cases_crime_category_id_fkey" FOREIGN KEY ("crime_category_id") REFERENCES "public"."crime_categories"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."crime_cases"
ADD CONSTRAINT "crime_cases_crime_id_fkey" FOREIGN KEY ("crime_id") REFERENCES "public"."crimes"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."crimes"
ADD CONSTRAINT "crimes_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "public"."cities"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."crimes"
ADD CONSTRAINT "crimes_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."demographics"
ADD CONSTRAINT "demographics_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "public"."cities"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."demographics"
ADD CONSTRAINT "demographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER TABLE ONLY "public"."districts"
ADD CONSTRAINT "districts_city_id_fkey" FOREIGN KEY ("city_id") REFERENCES "public"."cities"("id") ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY "public"."geographics"
ADD CONSTRAINT "geographics_district_id_fkey" FOREIGN KEY ("district_id") REFERENCES "public"."districts"("id") ON UPDATE CASCADE ON DELETE SET NULL;
ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "postgres";
GRANT USAGE ON SCHEMA "public" TO "anon";
GRANT USAGE ON SCHEMA "public" TO "authenticated";
GRANT USAGE ON SCHEMA "public" TO "service_role";
GRANT ALL ON SCHEMA "public" TO "prisma";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "prisma";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "prisma";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "prisma";
RESET ALL;

View File

@ -0,0 +1,189 @@
revoke delete on table "public"."_prisma_migrations" from "anon";
revoke insert on table "public"."_prisma_migrations" from "anon";
revoke references on table "public"."_prisma_migrations" from "anon";
revoke select on table "public"."_prisma_migrations" from "anon";
revoke trigger on table "public"."_prisma_migrations" from "anon";
revoke truncate on table "public"."_prisma_migrations" from "anon";
revoke update on table "public"."_prisma_migrations" from "anon";
revoke delete on table "public"."_prisma_migrations" from "authenticated";
revoke insert on table "public"."_prisma_migrations" from "authenticated";
revoke references on table "public"."_prisma_migrations" from "authenticated";
revoke select on table "public"."_prisma_migrations" from "authenticated";
revoke trigger on table "public"."_prisma_migrations" from "authenticated";
revoke truncate on table "public"."_prisma_migrations" from "authenticated";
revoke update on table "public"."_prisma_migrations" from "authenticated";
revoke delete on table "public"."_prisma_migrations" from "service_role";
revoke insert on table "public"."_prisma_migrations" from "service_role";
revoke references on table "public"."_prisma_migrations" from "service_role";
revoke select on table "public"."_prisma_migrations" from "service_role";
revoke trigger on table "public"."_prisma_migrations" from "service_role";
revoke truncate on table "public"."_prisma_migrations" from "service_role";
revoke update on table "public"."_prisma_migrations" from "service_role";
revoke delete on table "public"."cities" from "anon";
revoke insert on table "public"."cities" from "anon";
revoke references on table "public"."cities" from "anon";
revoke select on table "public"."cities" from "anon";
revoke trigger on table "public"."cities" from "anon";
revoke truncate on table "public"."cities" from "anon";
revoke update on table "public"."cities" from "anon";
revoke delete on table "public"."cities" from "authenticated";
revoke insert on table "public"."cities" from "authenticated";
revoke references on table "public"."cities" from "authenticated";
revoke select on table "public"."cities" from "authenticated";
revoke trigger on table "public"."cities" from "authenticated";
revoke truncate on table "public"."cities" from "authenticated";
revoke update on table "public"."cities" from "authenticated";
revoke delete on table "public"."cities" from "service_role";
revoke insert on table "public"."cities" from "service_role";
revoke references on table "public"."cities" from "service_role";
revoke select on table "public"."cities" from "service_role";
revoke trigger on table "public"."cities" from "service_role";
revoke truncate on table "public"."cities" from "service_role";
revoke update on table "public"."cities" from "service_role";
revoke delete on table "public"."contact_messages" from "anon";
revoke insert on table "public"."contact_messages" from "anon";
revoke references on table "public"."contact_messages" from "anon";
revoke select on table "public"."contact_messages" from "anon";
revoke trigger on table "public"."contact_messages" from "anon";
revoke truncate on table "public"."contact_messages" from "anon";
revoke update on table "public"."contact_messages" from "anon";
revoke delete on table "public"."contact_messages" from "authenticated";
revoke insert on table "public"."contact_messages" from "authenticated";
revoke references on table "public"."contact_messages" from "authenticated";
revoke select on table "public"."contact_messages" from "authenticated";
revoke trigger on table "public"."contact_messages" from "authenticated";
revoke truncate on table "public"."contact_messages" from "authenticated";
revoke update on table "public"."contact_messages" from "authenticated";
revoke delete on table "public"."contact_messages" from "service_role";
revoke insert on table "public"."contact_messages" from "service_role";
revoke references on table "public"."contact_messages" from "service_role";
revoke select on table "public"."contact_messages" from "service_role";
revoke trigger on table "public"."contact_messages" from "service_role";
revoke truncate on table "public"."contact_messages" from "service_role";
revoke update on table "public"."contact_messages" from "service_role";
revoke delete on table "public"."crime_cases" from "anon";
revoke insert on table "public"."crime_cases" from "anon";
revoke references on table "public"."crime_cases" from "anon";
revoke select on table "public"."crime_cases" from "anon";
revoke trigger on table "public"."crime_cases" from "anon";
revoke truncate on table "public"."crime_cases" from "anon";
revoke update on table "public"."crime_cases" from "anon";
revoke delete on table "public"."crime_cases" from "authenticated";
revoke insert on table "public"."crime_cases" from "authenticated";
revoke references on table "public"."crime_cases" from "authenticated";
revoke select on table "public"."crime_cases" from "authenticated";
revoke trigger on table "public"."crime_cases" from "authenticated";
revoke truncate on table "public"."crime_cases" from "authenticated";
revoke update on table "public"."crime_cases" from "authenticated";
revoke delete on table "public"."crime_cases" from "service_role";
revoke insert on table "public"."crime_cases" from "service_role";
revoke references on table "public"."crime_cases" from "service_role";
revoke select on table "public"."crime_cases" from "service_role";
revoke trigger on table "public"."crime_cases" from "service_role";
revoke truncate on table "public"."crime_cases" from "service_role";
revoke update on table "public"."crime_cases" from "service_role";
revoke delete on table "public"."crime_categories" from "anon";
revoke insert on table "public"."crime_categories" from "anon";
revoke references on table "public"."crime_categories" from "anon";
revoke select on table "public"."crime_categories" from "anon";
revoke trigger on table "public"."crime_categories" from "anon";
revoke truncate on table "public"."crime_categories" from "anon";
revoke update on table "public"."crime_categories" from "anon";
revoke delete on table "public"."crime_categories" from "authenticated";
revoke insert on table "public"."crime_categories" from "authenticated";
revoke references on table "public"."crime_categories" from "authenticated";
revoke select on table "public"."crime_categories" from "authenticated";
revoke trigger on table "public"."crime_categories" from "authenticated";
revoke truncate on table "public"."crime_categories" from "authenticated";
revoke update on table "public"."crime_categories" from "authenticated";
revoke delete on table "public"."crime_categories" from "service_role";
revoke insert on table "public"."crime_categories" from "service_role";
revoke references on table "public"."crime_categories" from "service_role";
revoke select on table "public"."crime_categories" from "service_role";
revoke trigger on table "public"."crime_categories" from "service_role";
revoke truncate on table "public"."crime_categories" from "service_role";
revoke update on table "public"."crime_categories" from "service_role";
revoke delete on table "public"."crimes" from "anon";
revoke insert on table "public"."crimes" from "anon";
revoke references on table "public"."crimes" from "anon";
revoke select on table "public"."crimes" from "anon";
revoke trigger on table "public"."crimes" from "anon";
revoke truncate on table "public"."crimes" from "anon";
revoke update on table "public"."crimes" from "anon";
revoke delete on table "public"."crimes" from "authenticated";
revoke insert on table "public"."crimes" from "authenticated";
revoke references on table "public"."crimes" from "authenticated";
revoke select on table "public"."crimes" from "authenticated";
revoke trigger on table "public"."crimes" from "authenticated";
revoke truncate on table "public"."crimes" from "authenticated";
revoke update on table "public"."crimes" from "authenticated";
revoke delete on table "public"."crimes" from "service_role";
revoke insert on table "public"."crimes" from "service_role";
revoke references on table "public"."crimes" from "service_role";
revoke select on table "public"."crimes" from "service_role";
revoke trigger on table "public"."crimes" from "service_role";
revoke truncate on table "public"."crimes" from "service_role";
revoke update on table "public"."crimes" from "service_role";
revoke delete on table "public"."demographics" from "anon";
revoke insert on table "public"."demographics" from "anon";
revoke references on table "public"."demographics" from "anon";
revoke select on table "public"."demographics" from "anon";
revoke trigger on table "public"."demographics" from "anon";
revoke truncate on table "public"."demographics" from "anon";
revoke update on table "public"."demographics" from "anon";
revoke delete on table "public"."demographics" from "authenticated";
revoke insert on table "public"."demographics" from "authenticated";
revoke references on table "public"."demographics" from "authenticated";
revoke select on table "public"."demographics" from "authenticated";
revoke trigger on table "public"."demographics" from "authenticated";
revoke truncate on table "public"."demographics" from "authenticated";
revoke update on table "public"."demographics" from "authenticated";
revoke delete on table "public"."demographics" from "service_role";
revoke insert on table "public"."demographics" from "service_role";
revoke references on table "public"."demographics" from "service_role";
revoke select on table "public"."demographics" from "service_role";
revoke trigger on table "public"."demographics" from "service_role";
revoke truncate on table "public"."demographics" from "service_role";
revoke update on table "public"."demographics" from "service_role";
revoke delete on table "public"."districts" from "anon";
revoke insert on table "public"."districts" from "anon";
revoke references on table "public"."districts" from "anon";
revoke select on table "public"."districts" from "anon";
revoke trigger on table "public"."districts" from "anon";
revoke truncate on table "public"."districts" from "anon";
revoke update on table "public"."districts" from "anon";
revoke delete on table "public"."districts" from "authenticated";
revoke insert on table "public"."districts" from "authenticated";
revoke references on table "public"."districts" from "authenticated";
revoke select on table "public"."districts" from "authenticated";
revoke trigger on table "public"."districts" from "authenticated";
revoke truncate on table "public"."districts" from "authenticated";
revoke update on table "public"."districts" from "authenticated";
revoke delete on table "public"."districts" from "service_role";
revoke insert on table "public"."districts" from "service_role";
revoke references on table "public"."districts" from "service_role";
revoke select on table "public"."districts" from "service_role";
revoke trigger on table "public"."districts" from "service_role";
revoke truncate on table "public"."districts" from "service_role";
revoke update on table "public"."districts" from "service_role";
revoke delete on table "public"."geographics" from "anon";
revoke insert on table "public"."geographics" from "anon";
revoke references on table "public"."geographics" from "anon";
revoke select on table "public"."geographics" from "anon";
revoke trigger on table "public"."geographics" from "anon";
revoke truncate on table "public"."geographics" from "anon";
revoke update on table "public"."geographics" from "anon";
revoke delete on table "public"."geographics" from "authenticated";
revoke insert on table "public"."geographics" from "authenticated";
revoke references on table "public"."geographics" from "authenticated";
revoke select on table "public"."geographics" from "authenticated";
revoke trigger on table "public"."geographics" from "authenticated";
revoke truncate on table "public"."geographics" from "authenticated";
revoke update on table "public"."geographics" from "authenticated";
revoke delete on table "public"."geographics" from "service_role";
revoke insert on table "public"."geographics" from "service_role";
revoke references on table "public"."geographics" from "service_role";
revoke select on table "public"."geographics" from "service_role";
revoke trigger on table "public"."geographics" from "service_role";
revoke truncate on table "public"."geographics" from "service_role";
revoke update on table "public"."geographics" from "service_role";

View File

@ -0,0 +1,55 @@
-- Create a table for public profiles
create table profiles (
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
full_name text,
avatar_url text,
website text,
constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.
alter table profiles
enable row level security;
create policy "Public profiles are viewable by everyone." on profiles
for select using (true);
create policy "Users can insert their own profile." on profiles
for insert with check ((select auth.uid()) = id);
create policy "Users can update own profile." on profiles
for update using ((select auth.uid()) = id);
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger
set search_path = ''
as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
for select using (bucket_id = 'avatars');
create policy "Anyone can upload an avatar." on storage.objects
for insert with check (bucket_id = 'avatars');
create policy "Anyone can update their own avatar." on storage.objects
for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');