add users table

This commit is contained in:
vergiLgood1 2025-02-27 00:58:32 +07:00
parent 3ba462d797
commit 39a0af5e0f
159 changed files with 3181 additions and 1500 deletions

View File

@ -1,7 +1,5 @@
{
"deno.enablePaths": [
"supabase/functions"
],
"deno.enablePaths": ["supabase/functions"],
"deno.lint": true,
"deno.unstable": [
"bare-node-builtins",
@ -19,6 +17,6 @@
"net"
],
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -1,107 +0,0 @@
"use server";
import AdminNotification from "@/components/email-templates/admin-notification";
import { render } from "@react-email/components";
import UserConfirmation from "@/components/email-templates/user-confirmation";
import { createClient } from "@/utils/supabase/server";
import { useResend } from "@/hooks/use-resend";
import { typeMessageMap } from "@/src/applications/entities/models/contact-us.model";
export async function sendContactEmail(formData: {
name: string;
email: string;
phone: string;
typeMessage: string;
message: string;
}) {
try {
// Initialize Supabase
const supabase = await createClient();
const { resend } = useResend();
// Get message type label
const messageTypeLabel =
typeMessageMap.get(formData.typeMessage) || "Unknown";
// Save to Supabase
const { data: contactData, error: contactError } = await supabase
.from("contact_messages")
.insert([
{
name: formData.name,
email: formData.email,
phone: formData.phone,
message_type: formData.typeMessage,
message_type_label: messageTypeLabel,
message: formData.message,
status: "new",
},
])
.select();
if (contactError) {
console.error("Error saving contact message to Supabase:", contactError);
return {
success: false,
error: "Failed to save your message. Please try again later.",
};
}
// Render admin email template
const adminEmailHtml = await render(
AdminNotification({
name: formData.name,
email: formData.email,
phone: formData.phone,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send email to admin
const { data: emailData, error: emailError } = await resend.emails.send({
from: "Contact Form <contact@backspacex.tech>",
to: ["xdamazon17@gmail.com"],
subject: `New Contact Form Submission: ${messageTypeLabel}`,
html: adminEmailHtml,
});
if (emailError) {
console.error("Error sending email via Resend:", emailError);
// Note: We don't return error here since the data is already saved to Supabase
}
const userEmailHtml = await render(
UserConfirmation({
name: formData.name,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send confirmation email to user
const { data: confirmationData, error: confirmationError } =
await resend.emails.send({
from: "Your Company <support@backspacex.tech>",
to: [formData.email],
subject: "Thank you for contacting us",
html: userEmailHtml,
});
if (confirmationError) {
console.error("Error sending confirmation email:", confirmationError);
// Note: We don't return error here either
}
return {
success: true,
message: "Your message has been sent successfully!",
};
} catch (error) {
console.error("Unexpected error in sendContactEmail:", error);
return {
success: false,
error: "An unexpected error occurred. Please try again later.",
};
}
}

View File

@ -1,38 +0,0 @@
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();
if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});
if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password"
);
}
if (callbackUrl) {
return redirect(callbackUrl);
}
return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password."
);
};

View File

@ -1,41 +0,0 @@
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const resetPasswordAction = async (formData: FormData) => {
const supabase = await createClient();
const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;
if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required"
);
}
if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match"
);
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed"
);
}
encodedRedirect("success", "/protected/reset-password", "Password updated");
};

View File

@ -1,39 +0,0 @@
"use server";
import { createClient } from "@/utils/supabase/server";
export const checkSession = async () => {
const supabase = await createClient();
try {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
return {
success: false,
error: error.message,
};
}
if (session) {
return {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
};
}
return {
success: false,
message: "No active session",
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
};
}
};

View File

@ -1,53 +0,0 @@
"use server";
import { createClient } from "@/utils/supabase/server";
export const signInAction = async (formData: { email: string }) => {
const supabase = await createClient();
const encodeEmail = encodeURIComponent(formData.email);
try {
// First, check for existing session
const {
data: { session },
error: sessionError,
} = await supabase.auth.getSession();
// If there's an active session and the email matches
if (session && session.user.email === formData.email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
};
}
// If no active session or different email, proceed with OTP
const { data, error } = await supabase.auth.signInWithOtp({
email: formData.email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return {
success: false,
error: error.message,
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
}
return {
success: true,
message: "OTP has been sent to your email",
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
redirectTo: "/sign-in",
};
}
};

View File

@ -1,10 +0,0 @@
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};

View File

@ -1,38 +0,0 @@
import { encodedRedirect } from "@/utils/utils";
import { createClient } from "@/utils/supabase/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
if (!email || !password) {
return encodedRedirect(
"error",
"/sign-up",
"Email and password are required",
);
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link.",
);
}
};

View File

@ -1,32 +0,0 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { redirect } from "next/navigation";
export const verifyOtpAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const token = formData.get("token") as string;
const supabase = await createClient();
console.log("email", email);
console.log("token", token);
if (!email || !token) {
redirect("/error?message=Email and OTP are required");
}
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email,
token,
type: "email",
});
if (error) {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
}
return redirect("/protected/dashboard?message=OTP verified successfully");
};

View File

@ -1,211 +0,0 @@
"use server";
import {
NavItems,
navItemsDeleteSchema,
NavItemsInsert,
navItemsInsertSchema,
navItemsUpdateSchema,
} from "@/src/applications/entities/models/nav-items.model";
import { NavItemsRepository } from "@/src/applications/repositories/nav-items.repository";
import { NavItemsRepositoryImpl } from "@/src/infrastructure/repositories/nav-items.repository.impl";
import { revalidatePath } from "next/cache";
import { z } from "zod";
// Initialize repository
const navItemsRepo: NavItemsRepository = new NavItemsRepositoryImpl();
/**
* Get all navigation items
*/
export async function getNavItems() {
return await navItemsRepo.getNavItems();
}
/**
* Create a new navigation item
*/
export async function createNavItem(
formData: FormData | Record<string, NavItems>
) {
try {
const data =
formData instanceof FormData
? Object.fromEntries(formData.entries())
: formData;
// Parse and validate input data
const validatedData = navItemsInsertSchema.parse(data);
// Call repository method (assuming it exists)
const result = await navItemsRepo.createNavItems(validatedData);
// Revalidate cache if successful
if (result.success) {
revalidatePath("/admin/navigation");
}
return result;
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.reduce(
(acc, curr) => {
acc[curr.path.join(".")] = curr.message;
return acc;
},
{} as Record<string, string>
),
message: "Validation failed",
};
}
return {
success: false,
error: "Failed to create navigation item",
message:
error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
/**
* Update an existing navigation item
*/
export async function updateNavItem(
id: string,
formData: FormData | Record<string, unknown>
) {
try {
const data =
formData instanceof FormData
? Object.fromEntries(formData.entries())
: formData;
// Parse and validate input data
const validatedData = navItemsUpdateSchema.parse(data);
// Call repository method (assuming it exists)
const result = await navItemsRepo.updateNavItems(id, validatedData);
// Revalidate cache if successful
if (result.success) {
revalidatePath("/admin/navigation");
}
return result;
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.reduce(
(acc, curr) => {
acc[curr.path.join(".")] = curr.message;
return acc;
},
{} as Record<string, string>
),
message: "Validation failed",
};
}
return {
success: false,
error: "Failed to update navigation item",
message:
error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
/**
* Delete a navigation item
*/
export async function deleteNavItem(id: string) {
try {
// Parse and validate input
const validatedData = navItemsDeleteSchema.parse({ id });
// Call repository method (assuming it exists)
const result = await navItemsRepo.deleteNavItems(validatedData.id);
// Revalidate cache if successful
if (result.success) {
revalidatePath("/admin/navigation");
}
return result;
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.reduce(
(acc, curr) => {
acc[curr.path.join(".")] = curr.message;
return acc;
},
{} as Record<string, string>
),
message: "Validation failed",
};
}
return {
success: false,
error: "Failed to delete navigation item",
message:
error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
// /**
// * Toggle active status of a navigation item
// */
// export async function toggleNavItemActive(id: string) {
// try {
// // Call repository method (assuming it exists)
// const result = await navItemsRepo.toggleActive(id);
// // Revalidate cache if successful
// if (result.success) {
// revalidatePath("/admin/navigation");
// }
// return result;
// } catch (error) {
// return {
// success: false,
// error: "Failed to toggle navigation item status",
// message:
// error instanceof Error ? error.message : "Unknown error occurred",
// };
// }
// }
// /**
// * Update navigation items order
// */
// export async function updateNavItemsOrder(
// items: { id: string; order_seq: number }[]
// ) {
// try {
// // Call repository method (assuming it exists)
// const result = await navItemsRepo.updateOrder(items);
// // Revalidate cache if successful
// if (result.success) {
// revalidatePath("/admin/navigation");
// }
// return result;
// } catch (error) {
// return {
// success: false,
// error: "Failed to update navigation order",
// message:
// error instanceof Error ? error.message : "Unknown error occurred",
// };
// }
// }

View File

@ -8,23 +8,23 @@ import {
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
} from "@/app/_components/ui/card";
import { Input } from "@/app/_components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "../ui/textarea";
import { SubmitButton } from "../submit-button";
} from "@/app/_components/ui/select";
import { Textarea } from "../../../_components/ui/textarea";
import { SubmitButton } from "../../../_components/submit-button";
import Link from "next/link";
import { TValidator } from "@/utils/validator";
import { useContactForm } from "@/src/infrastructure/hooks/use-contact-us-form";
import { FormField } from "../form-field";
import { typeMessage } from "@/src/applications/entities/models/contact-us.model";
import { Form } from "../ui/form";
import { FormField } from "../../../_components/form-field";
import { typeMessage } from "@/src/entities/models/contact-us.model";
import { Form } from "../../../_components/ui/form";
import { useContactForm } from "@/hooks/use-contact-us-form";
export function ContactUsForm() {
const {

View File

@ -5,8 +5,8 @@ import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import { toast } from "@/app/_hooks/use-toast";
import { Button } from "@/app/_components/ui/button";
import {
Form,
FormControl,
@ -15,8 +15,8 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
} from "@/app/_components/ui/form";
import { Input } from "@/app/_components/ui/input";
import {
Card,
CardContent,
@ -24,9 +24,9 @@ import {
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { SubmitButton } from "@/components/submit-button";
} from "@/app/_components/ui/card";
import Link from "next/link";
import { SubmitButton } from "@/app/_components/submit-button";
const FormSchema = z.object({
email: z.string().email({

View File

@ -1,14 +1,14 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import { Github, Lock } from "lucide-react";
import Link from "next/link";
import { SubmitButton } from "../submit-button";
import { SubmitButton } from "../../../_components/submit-button";
import { FormField } from "../form-field";
import { useSignInForm } from "@/src/infrastructure/hooks/use-signin";
import { FormField } from "../../../_components/form-field";
import { useSignInForm } from "@/hooks/use-signin";
export function LoginForm2({
className,

View File

@ -1,13 +1,13 @@
// import { cn } from "@/lib/utils";
// import { Button } from "@/components/ui/button";
// import { Button } from "@/app/_components/ui/button";
// import {
// Card,
// CardContent,
// CardDescription,
// CardHeader,
// CardTitle,
// } from "@/components/ui/card";
// import { Input } from "@/components/ui/input";
// } from "@/app/_components/ui/card";
// import { Input } from "@/app/_components/ui/input";
// import { Label } from "@/components/ui/label";
// import { SubmitButton } from "../submit-button";
// import { signInAction } from "@/actions/auth/sign-in";

View File

@ -4,8 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import { toast } from "@/app/_hooks/use-toast";
import { Button } from "@/app/_components/ui/button";
import {
Form,
FormControl,
@ -14,20 +14,20 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
} from "@/app/_components/ui/form";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { SubmitButton } from "../submit-button";
} from "@/app/_components/ui/input-otp";
import { SubmitButton } from "../../../_components/submit-button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
} from "../../../_components/ui/card";
import { cn } from "@/lib/utils";
const FormSchema = z.object({

View File

@ -3,7 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "@/hooks/use-toast";
import { toast } from "@/app/_hooks/use-toast";
import {
Form,
FormControl,
@ -11,24 +11,24 @@ import {
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
} from "@/app/_components/ui/form";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { SubmitButton } from "../submit-button";
} from "@/app/_components/ui/input-otp";
import { SubmitButton } from "../../../_components/submit-button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../ui/card";
} from "../../../_components/ui/card";
import { cn } from "@/lib/utils";
import { verifyOtpAction } from "@/actions/auth/verify-otp";
import { useSearchParams } from "next/navigation";
import { Input } from "../ui/input";
import { Input } from "../../../_components/ui/input";
const FormSchema = z.object({
token: z.string().min(6, {

View File

@ -0,0 +1,336 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { encodedRedirect } from "@/utils/utils";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import AdminNotification from "../_components/email-templates/admin-notification";
import UserConfirmation from "../_components/email-templates/user-confirmation";
import { render } from "@react-email/components";
import { useResend } from "../_hooks/use-resend";
import { typeMessageMap } from "@/src/entities/models/contact-us.model";
export const signInAction = async (formData: { email: string }) => {
const supabase = await createClient();
const encodeEmail = encodeURIComponent(formData.email);
try {
// First, check for existing session
const {
data: { session },
error: sessionError,
} = await supabase.auth.getSession();
// If there's an active session and the email matches
if (session && session.user.email === formData.email) {
return {
success: true,
message: "Already logged in",
redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users
};
}
// If no active session or different email, proceed with OTP
const { data, error } = await supabase.auth.signInWithOtp({
email: formData.email,
options: {
shouldCreateUser: false,
},
});
if (error) {
return {
success: false,
error: error.message,
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
}
return {
success: true,
message: "OTP has been sent to your email",
redirectTo: `/verify-otp?email=${encodeEmail}`,
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
redirectTo: "/sign-in",
};
}
};
export const signUpAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const password = formData.get("password")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
if (!email || !password) {
return encodedRedirect(
"error",
"/sign-up",
"Email and password are required",
);
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return encodedRedirect("error", "/sign-up", error.message);
} else {
return encodedRedirect(
"success",
"/sign-up",
"Thanks for signing up! Please check your email for a verification link.",
);
}
};
export const checkSession = async () => {
const supabase = await createClient();
try {
const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) {
return {
success: false,
error: error.message,
};
}
if (session) {
return {
success: true,
session,
redirectTo: "/protected/dashboard", // or your preferred authenticated route
};
}
return {
success: false,
message: "No active session",
};
} catch (error) {
return {
success: false,
error: "An unexpected error occurred",
};
}
};
export const forgotPasswordAction = async (formData: FormData) => {
const email = formData.get("email")?.toString();
const supabase = await createClient();
const origin = (await headers()).get("origin");
const callbackUrl = formData.get("callbackUrl")?.toString();
if (!email) {
return encodedRedirect("error", "/forgot-password", "Email is required");
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/protected/reset-password`,
});
if (error) {
console.error(error.message);
return encodedRedirect(
"error",
"/forgot-password",
"Could not reset password"
);
}
if (callbackUrl) {
return redirect(callbackUrl);
}
return encodedRedirect(
"success",
"/forgot-password",
"Check your email for a link to reset your password."
);
};
export const resetPasswordAction = async (formData: FormData) => {
const supabase = await createClient();
const password = formData.get("password") as string;
const confirmPassword = formData.get("confirmPassword") as string;
if (!password || !confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password and confirm password are required"
);
}
if (password !== confirmPassword) {
encodedRedirect(
"error",
"/protected/reset-password",
"Passwords do not match"
);
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
encodedRedirect(
"error",
"/protected/reset-password",
"Password update failed"
);
}
encodedRedirect("success", "/protected/reset-password", "Password updated");
};
export const verifyOtpAction = async (formData: FormData) => {
const email = formData.get("email") as string;
const token = formData.get("token") as string;
const supabase = await createClient();
console.log("email", email);
console.log("token", token);
if (!email || !token) {
redirect("/error?message=Email and OTP are required");
}
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email,
token,
type: "email",
});
if (error) {
return redirect(`/verify-otp?error=${encodeURIComponent(error.message)}`);
}
return redirect("/protected/dashboard?message=OTP verified successfully");
};
export async function sendContactEmail(formData: {
name: string;
email: string;
phone: string;
typeMessage: string;
message: string;
}) {
try {
// Initialize Supabase
const supabase = await createClient();
const { resend } = useResend();
// Get message type label
const messageTypeLabel =
typeMessageMap.get(formData.typeMessage) || "Unknown";
// Save to Supabase
const { data: contactData, error: contactError } = await supabase
.from("contact_messages")
.insert([
{
name: formData.name,
email: formData.email,
phone: formData.phone,
message_type: formData.typeMessage,
message_type_label: messageTypeLabel,
message: formData.message,
status: "new",
},
])
.select();
if (contactError) {
console.error("Error saving contact message to Supabase:", contactError);
return {
success: false,
error: "Failed to save your message. Please try again later.",
};
}
// Render admin email template
const adminEmailHtml = await render(
AdminNotification({
name: formData.name,
email: formData.email,
phone: formData.phone,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send email to admin
const { data: emailData, error: emailError } = await resend.emails.send({
from: "Contact Form <contact@backspacex.tech>",
to: ["xdamazon17@gmail.com"],
subject: `New Contact Form Submission: ${messageTypeLabel}`,
html: adminEmailHtml,
});
if (emailError) {
console.error("Error sending email via Resend:", emailError);
// Note: We don't return error here since the data is already saved to Supabase
}
const userEmailHtml = await render(
UserConfirmation({
name: formData.name,
messageType: messageTypeLabel,
message: formData.message,
})
);
// Send confirmation email to user
const { data: confirmationData, error: confirmationError } =
await resend.emails.send({
from: "Your Company <support@backspacex.tech>",
to: [formData.email],
subject: "Thank you for contacting us",
html: userEmailHtml,
});
if (confirmationError) {
console.error("Error sending confirmation email:", confirmationError);
// Note: We don't return error here either
}
return {
success: true,
message: "Your message has been sent successfully!",
};
} catch (error) {
console.error("Unexpected error in sendContactEmail:", error);
return {
success: false,
error: "An unexpected error occurred. Please try again later.",
};
}
}
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
};

View File

@ -1,5 +1,5 @@
import { ContactUsForm } from "@/components/auth/contact-us";
import { Button } from "@/components/ui/button";
import { ContactUsForm } from "@/app/(auth-pages)/_components/auth/contact-us";
import { Button } from "@/app/_components/ui/button";
import { GalleryVerticalEnd, Globe } from "lucide-react";
export default async function ContactAdminPage() {

View File

@ -1,6 +1,4 @@
import RecoveryEmailForm from "@/components/auth/email-recovery";
import { VerifyOtpForm } from "@/components/auth/verify-otp-form";
import RecoveryEmailForm from "@/app/(auth-pages)/_components/auth/email-recovery";
import { GalleryVerticalEnd } from "lucide-react";
export default async function VerifyOtpPage() {

View File

@ -1,10 +1,12 @@
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
import { forgotPasswordAction } from "@/actions/auth/forgot-password";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
import { SubmitButton } from "@/app/_components/submit-button";
import { FormMessage, Message } from "@/app/_components/form-message";
import { forgotPasswordAction } from "../actions";
export default async function ForgotPassword(props: {
searchParams: Promise<Message>;

View File

@ -1,5 +1,6 @@
import { checkSession } from "@/actions/auth/session";
import { redirect } from "next/navigation";
import { checkSession } from "./actions";
export default async function Layout({
children,

View File

@ -1,11 +1,7 @@
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { GalleryVerticalEnd, Book, Globe } from "lucide-react";
import Link from "next/link";
import { LoginForm2 } from "@/components/auth/login-form-2";
import { Message } from "@/app/_components/form-message";
import { Button } from "@/app/_components/ui/button";
import { GalleryVerticalEnd, Globe } from "lucide-react";
import { LoginForm2 } from "@/app/(auth-pages)/_components/auth/login-form-2";
export default async function Login(props: { searchParams: Promise<Message> }) {
const searchParams = await props.searchParams;

View File

@ -1,7 +1,7 @@
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { FormMessage, Message } from "@/app/_components/form-message";
import { SubmitButton } from "@/app/_components/submit-button";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
import Link from "next/link";
import { SmtpMessage } from "../smtp-message";
import { signUpAction } from "@/actions/auth/sign-up-action";

View File

@ -1,4 +1,4 @@
import { VerifyOtpForm } from "@/components/auth/verify-otp-form";
import { VerifyOtpForm } from "@/app/(auth-pages)/_components/auth/verify-otp-form";
import { GalleryVerticalEnd } from "lucide-react";

View File

@ -1,22 +1,35 @@
"use client"
"use client";
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react"
import { Input } from "@/components/ui/input"
import { motion, AnimatePresence } from "framer-motion"
import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines } from "lucide-react"
import useDebounce from "@/hooks/use-debounce"
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { Input } from "@/app/_components/ui/input";
import { motion, AnimatePresence } from "framer-motion";
import {
Search,
Send,
BarChart2,
Globe,
Video,
PlaneTakeoff,
AudioLines,
} from "lucide-react";
import useDebounce from "@/app/_hooks/use-debounce";
interface Action {
id: string
label: string
icon: React.ReactNode
description?: string
short?: string
end?: string
id: string;
label: string;
icon: React.ReactNode;
description?: string;
short?: string;
end?: string;
}
interface SearchResult {
actions: Action[]
actions: Action[];
}
const allActions = [
@ -60,49 +73,49 @@ const allActions = [
short: "",
end: "Command",
},
]
];
interface ActionSearchBarProps {
actions?: Action[]
autoFocus?: boolean
isFloating?: boolean
actions?: Action[];
autoFocus?: boolean;
isFloating?: boolean;
}
const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
({ actions = allActions, autoFocus = false, isFloating = false }, ref) => {
const [query, setQuery] = useState("")
const [result, setResult] = useState<SearchResult | null>(null)
const [isFocused, setIsFocused] = useState(autoFocus)
const [selectedAction, setSelectedAction] = useState<Action | null>(null)
const debouncedQuery = useDebounce(query, 200)
const inputRef = React.useRef<HTMLInputElement>(null)
const [query, setQuery] = useState("");
const [result, setResult] = useState<SearchResult | null>(null);
const [isFocused, setIsFocused] = useState(autoFocus);
const [selectedAction, setSelectedAction] = useState<Action | null>(null);
const debouncedQuery = useDebounce(query, 200);
const inputRef = React.useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus()
inputRef.current.focus();
}
}, [autoFocus])
}, [autoFocus]);
useEffect(() => {
if (!debouncedQuery) {
setResult({ actions: allActions })
return
setResult({ actions: allActions });
return;
}
const normalizedQuery = debouncedQuery.toLowerCase().trim()
const normalizedQuery = debouncedQuery.toLowerCase().trim();
const filteredActions = allActions.filter((action) => {
const searchableText = action.label.toLowerCase()
return searchableText.includes(normalizedQuery)
})
const searchableText = action.label.toLowerCase();
return searchableText.includes(normalizedQuery);
});
setResult({ actions: filteredActions })
}, [debouncedQuery])
setResult({ actions: filteredActions });
}, [debouncedQuery]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value)
}
setQuery(e.target.value);
};
const container = {
hidden: { opacity: 0, height: 0 },
@ -128,7 +141,7 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
},
},
},
}
};
const item = {
hidden: { opacity: 0, y: 20 },
@ -146,10 +159,12 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
duration: 0.2,
},
},
}
};
return (
<div className={`relative w-full ${isFloating ? "bg-background rounded-lg shadow-lg" : ""}`}>
<div
className={`relative w-full ${isFloating ? "bg-background rounded-lg shadow-lg" : ""}`}
>
<div className="relative">
<Input
ref={inputRef}
@ -158,7 +173,9 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
value={query}
onChange={handleInputChange}
onFocus={() => setIsFocused(true)}
onBlur={() => !isFloating && setTimeout(() => setIsFocused(false), 200)}
onBlur={() =>
!isFloating && setTimeout(() => setIsFocused(false), 200)
}
className="pl-3 pr-9 py-1.5 h-9 text-sm rounded-lg focus-visible:ring-offset-0"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4">
@ -209,13 +226,21 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
<div className="flex items-center gap-2 justify-between">
<div className="flex items-center gap-2">
<span className="text-gray-500">{action.icon}</span>
<span className="text-sm font-medium">{action.label}</span>
<span className="text-xs text-muted-foreground">{action.description}</span>
<span className="text-sm font-medium">
{action.label}
</span>
<span className="text-xs text-muted-foreground">
{action.description}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{action.short}</span>
<span className="text-xs text-muted-foreground text-right">{action.end}</span>
<span className="text-xs text-muted-foreground">
{action.short}
</span>
<span className="text-xs text-muted-foreground text-right">
{action.end}
</span>
</div>
</motion.li>
))}
@ -230,11 +255,10 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
)}
</AnimatePresence>
</div>
)
},
)
);
}
);
ActionSearchBar.displayName = "ActionSearchBar"
export default ActionSearchBar
ActionSearchBar.displayName = "ActionSearchBar";
export default ActionSearchBar;

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import ActionSearchBar from "@/components/action-search-bar";
import ActionSearchBar from "@/app/_components/action-search-bar";
export default function FloatingActionSearchBar() {
const [isOpen, setIsOpen] = useState(false);

View File

@ -2,18 +2,22 @@
import * as React from "react";
import { Inbox, Search, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Button } from "@/app/_components/ui/button";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
} from "@/app/_components/ui/sheet";
import { Input } from "@/app/_components/ui/input";
import { ScrollArea } from "@/app/_components/ui/scroll-area";
import { Badge } from "@/app/_components/ui/badge";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
interface InboxDrawerProps {
showTitle?: boolean;
@ -95,7 +99,6 @@ const sampleMails: MailMessage[] = [
date: "4 days ago",
read: true,
},
];
const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({

View File

@ -1,6 +1,6 @@
"use client";
import { Button } from "@/components/ui/button";
import { Button } from "@/app/_components/ui/button";
import { type ComponentProps } from "react";
import { useFormStatus } from "react-dom";

View File

@ -1,7 +1,7 @@
"use client"
"use client";
import * as React from "react"
import { ChevronsUpDown, Plus } from "lucide-react"
import * as React from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import {
DropdownMenu,
@ -11,25 +11,25 @@ import {
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/app/_components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
} from "@/app/_components/ui/sidebar";
export function TeamSwitcher({
teams,
}: {
teams: {
name: string
logo: React.ElementType
plan: string
}[]
name: string;
logo: React.ElementType;
plan: string;
}[];
}) {
const { isMobile } = useSidebar()
const [activeTeam, setActiveTeam] = React.useState(teams[0])
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
return (
<SidebarMenu>
@ -85,5 +85,5 @@ export function TeamSwitcher({
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
);
}

View File

@ -2,14 +2,14 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { Button } from "@/app/_components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from "@/app/_components/ui/dropdown-menu";
import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState, useMemo } from "react";
@ -78,7 +78,7 @@ const ThemeSwitcherComponent = ({
className="flex gap-2"
value={option.value}
>
{/* <option.icon size={ICON_SIZE} className="text-muted-foreground" /> */}
<option.icon size={ICON_SIZE} className="text-muted-foreground" />
<span>{option.label}</span>
</DropdownMenuRadioItem>
))}

View File

@ -13,7 +13,7 @@ import {
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Label } from "@/app/_components/ui/label"
const Form = FormProvider

View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -1,58 +1,58 @@
"use client"
"use client";
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import { useIsMobile } from "@/app/_hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import { Separator } from "@/app/_components/ui/separator";
import { Sheet, SheetContent } from "@/app/_components/ui/sheet";
import { Skeleton } from "@/app/_components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
} from "@/app/_components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null)
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext)
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
@ -67,34 +67,34 @@ const SidebarProvider = React.forwardRef<
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState)
setOpenProp(openState);
} else {
_setOpen(openState)
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
)
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@ -103,18 +103,18 @@ const SidebarProvider = React.forwardRef<
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>(
() => ({
@ -127,7 +127,7 @@ const SidebarProvider = React.forwardRef<
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
);
return (
<SidebarContext.Provider value={contextValue}>
@ -151,17 +151,17 @@ const SidebarProvider = React.forwardRef<
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
);
}
)
SidebarProvider.displayName = "SidebarProvider"
);
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(
(
@ -175,7 +175,7 @@ const Sidebar = React.forwardRef<
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
@ -189,7 +189,7 @@ const Sidebar = React.forwardRef<
>
{children}
</div>
)
);
}
if (isMobile) {
@ -209,7 +209,7 @@ const Sidebar = React.forwardRef<
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
);
}
return (
@ -254,16 +254,16 @@ const Sidebar = React.forwardRef<
</div>
</div>
</div>
)
);
}
)
Sidebar.displayName = "Sidebar"
);
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
const { toggleSidebar } = useSidebar();
return (
<Button
@ -273,23 +273,23 @@ const SidebarTrigger = React.forwardRef<
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
);
});
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
const { toggleSidebar } = useSidebar();
return (
<button
@ -310,9 +310,9 @@ const SidebarRail = React.forwardRef<
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
);
});
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<
HTMLDivElement,
@ -328,9 +328,9 @@ const SidebarInset = React.forwardRef<
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
@ -346,9 +346,9 @@ const SidebarInput = React.forwardRef<
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
);
});
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<
HTMLDivElement,
@ -361,9 +361,9 @@ const SidebarHeader = React.forwardRef<
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
);
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<
HTMLDivElement,
@ -376,9 +376,9 @@ const SidebarFooter = React.forwardRef<
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
);
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
@ -391,9 +391,9 @@ const SidebarSeparator = React.forwardRef<
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
);
});
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<
HTMLDivElement,
@ -409,9 +409,9 @@ const SidebarContent = React.forwardRef<
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<
HTMLDivElement,
@ -424,15 +424,15 @@ const SidebarGroup = React.forwardRef<
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
const Comp = asChild ? Slot : "div";
return (
<Comp
@ -445,15 +445,15 @@ const SidebarGroupLabel = React.forwardRef<
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
);
});
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@ -468,9 +468,9 @@ const SidebarGroupAction = React.forwardRef<
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
);
});
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
@ -482,8 +482,8 @@ const SidebarGroupContent = React.forwardRef<
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
));
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<
HTMLUListElement,
@ -495,8 +495,8 @@ const SidebarMenu = React.forwardRef<
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
@ -508,8 +508,8 @@ const SidebarMenuItem = React.forwardRef<
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
@ -531,14 +531,14 @@ const sidebarMenuButtonVariants = cva(
size: "default",
},
}
)
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
@ -553,8 +553,8 @@ const SidebarMenuButton = React.forwardRef<
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
@ -565,16 +565,16 @@ const SidebarMenuButton = React.forwardRef<
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
);
if (!tooltip) {
return button
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
};
}
return (
@ -587,19 +587,19 @@ const SidebarMenuButton = React.forwardRef<
{...tooltip}
/>
</Tooltip>
)
);
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
);
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@ -619,9 +619,9 @@ const SidebarMenuAction = React.forwardRef<
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
@ -641,19 +641,19 @@ const SidebarMenuBadge = React.forwardRef<
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
));
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
@ -678,9 +678,9 @@ const SidebarMenuSkeleton = React.forwardRef<
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
@ -696,24 +696,24 @@ const SidebarMenuSub = React.forwardRef<
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
));
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
>(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
const Comp = asChild ? Slot : "a";
return (
<Comp
@ -731,9 +731,9 @@ const SidebarMenuSubButton = React.forwardRef<
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
@ -760,4 +760,4 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
};

View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -1,6 +1,6 @@
"use client";
import { useToast } from "@/hooks/use-toast";
import { useToast } from "@/app/_hooks/use-toast";
import {
Toast,
ToastClose,
@ -8,7 +8,7 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast";
} from "@/app/_components/ui/toast";
export function Toaster() {
const { toasts } = useToast();

View File

@ -6,7 +6,7 @@ import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
} from "@/app/_components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

View File

@ -1,15 +1,9 @@
import DeployButton from "@/components/deploy-button";
import { EnvVarWarning } from "@/components/env-var-warning";
import HeaderAuth from "@/components/header-auth";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import { Geist } from "next/font/google";
import { ThemeProvider } from "next-themes";
import Link from "next/link";
import "./globals.css";
import SupabaseLogo from "@/components/logo/supabase-logo";
import SigapLogo from "@/components/logo/sigap-logo";
import { Toaster } from "@/components/ui/toaster";
import { Toaster } from "./_components/ui/toaster";
const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
@ -17,8 +11,8 @@ const defaultUrl = process.env.VERCEL_URL
export const metadata = {
metadataBase: new URL(defaultUrl),
title: "Next.js and Supabase Starter Kit",
description: "The fastest way to build apps with Next.js and Supabase",
title: "Sigap",
description: "Sigap is a platform for managing your data.",
};
const geistSans = Geist({
@ -31,7 +25,6 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={geistSans.className} suppressHydrationWarning>
<body className="bg-background text-foreground">

View File

@ -1,7 +1,7 @@
import Hero from "@/components/hero";
import ConnectSupabaseSteps from "@/components/tutorial/connect-supabase-steps";
import SignUpUserSteps from "@/components/tutorial/sign-up-user-steps";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import SignUpUserSteps from "./_components/tutorial/sign-up-user-steps";
import ConnectSupabaseSteps from "./_components/tutorial/connect-supabase-steps";
import Hero from "@/app/_components/hero";
export default async function Home() {
return (

View File

@ -0,0 +1,37 @@
"use client";
import * as React from "react";
import { NavMain } from "@/app/protected/(admin-pages)/_components/navigations/nav-main";
import { NavReports } from "@/app/protected/(admin-pages)/_components/navigations/nav-report";
import { NavUser } from "@/app/protected/(admin-pages)/_components/navigations/nav-user";
import { TeamSwitcher } from "@/app/_components/team-switcher";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@/app/_components/ui/sidebar";
import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/data/nav";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={navData.teams} />
</SidebarHeader>
<SidebarContent>
<NavPreMain items={navData.NavPreMain} />
<NavMain items={navData.navMain} />
<NavReports reports={navData.reports} />
</SidebarContent>
<SidebarFooter>
<NavUser user={navData.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}

View File

@ -6,7 +6,7 @@ import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
} from "@/app/_components/ui/collapsible";
import {
SidebarGroup,
SidebarGroupLabel,
@ -14,10 +14,10 @@ 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;

View File

@ -0,0 +1,54 @@
"use client";
import type React from "react";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} 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";
interface NavItem {
title: string;
url: string;
icon: React.ElementType;
items?: NavItem[];
}
function NavItemComponent({ item }: { item: NavItem }) {
const router = useNavigations();
const isActive = router.pathname === item.url;
return (
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<SidebarMenuButton tooltip={item.title} asChild>
<a href={item.url}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
interface NavPreMainProps {
items: NavItem[];
}
export function NavPreMain({ items }: NavPreMainProps) {
return (
<SidebarGroup>
<SidebarGroupLabel>Quick Access</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<NavItemComponent key={item.title} item={item} />
))}
</SidebarMenu>
</SidebarGroup>
);
}

View File

@ -1,4 +1,4 @@
"use client"
"use client";
import {
Folder,
@ -6,7 +6,7 @@ import {
MoreHorizontal,
Trash2,
type LucideIcon,
} from "lucide-react"
} from "lucide-react";
import {
DropdownMenu,
@ -14,7 +14,7 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/app/_components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupLabel,
@ -23,7 +23,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
} from "@/app/_components/ui/sidebar";
import * as TablerIcons from "@tabler/icons-react";

View File

@ -1,4 +1,4 @@
"use client"
"use client";
import {
BadgeCheck,
@ -7,13 +7,13 @@ import {
CreditCard,
LogOut,
Sparkles,
} from "lucide-react"
} from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
} from "@/app/_components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@ -22,13 +22,13 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
} from "@/app/_components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
} from "@/app/_components/ui/sidebar";
import {
IconBadgeCc,
IconBell,

View File

@ -0,0 +1,191 @@
"use client";
import { Button } from "@/app/_components/ui/button";
import {
Form,
FormControl,
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 {
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";
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.",
}),
role: z.enum(["admin", "staff", "user"]),
status: z.enum(["active", "inactive"]),
});
interface EditUserSheetProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
selectedUser: User | null;
onUserUpdate: (updatedUser: User) => void;
}
export function EditUserSheet({
isOpen,
onOpenChange,
selectedUser,
onUserUpdate,
}: EditUserSheetProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstName: selectedUser?.firstName || "",
lastName: selectedUser?.lastName || "",
email: selectedUser?.email || "",
role: selectedUser?.role || "user",
status: selectedUser?.status || "inactive",
},
});
const onSubmit = useCallback(
(values: z.infer<typeof formSchema>) => {
if (!selectedUser) return;
const updatedUser: User = {
...selectedUser,
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
role: values.role,
status: values.status,
};
onUserUpdate(updatedUser);
onOpenChange(false);
},
[selectedUser, onUserUpdate, onOpenChange]
);
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Edit User</SheetTitle>
<SheetDescription>
Make changes to the user profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First name</FormLabel>
<FormControl>
<Input placeholder="First name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last name</FormLabel>
<FormControl>
<Input placeholder="Last name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="staff">Staff</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
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>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save changes</Button>
</form>
</Form>
</SheetContent>
</Sheet>
);
}

View File

@ -0,0 +1,45 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
import { Users, UserCheck, UserX } from 'lucide-react'
export function UserStats() {
return (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Users</CardTitle>
<UserCheck className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,105</div>
<p className="text-xs text-muted-foreground">
89.5% of total users
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Inactive Users</CardTitle>
<UserX className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">129</div>
<p className="text-xs text-muted-foreground">
10.5% of total users
</p>
</CardContent>
</Card>
</>
)
}

View File

@ -0,0 +1,585 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import {
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
ArrowUpDown,
ChevronDown,
Plus,
MoreVertical,
Filter,
} 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 { Input } from "@/app/_components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/app/_components/ui/table";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/app/_components/ui/avatar";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/app/_components/ui/select";
import { EditUserSheet } from "./edit-user";
const data: User[] = [
{
id: "1",
email: "john@example.com",
firstName: "John",
lastName: "Doe",
avatar: "/placeholder.svg?height=40&width=40",
role: "admin",
status: "active",
lastSignedIn: "2024-02-26",
},
{
id: "2",
email: "jane@example.com",
firstName: "Jane",
lastName: "Smith",
avatar: "/placeholder.svg?height=40&width=40",
role: "staff",
status: "active",
lastSignedIn: "2024-02-25",
},
{
id: "3",
email: "michael@example.com",
firstName: "Michael",
lastName: "Johnson",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "inactive",
lastSignedIn: "2024-02-20",
},
{
id: "4",
email: "sarah@example.com",
firstName: "Sarah",
lastName: "Williams",
avatar: "/placeholder.svg?height=40&width=40",
role: "staff",
status: "active",
lastSignedIn: "2024-02-24",
},
{
id: "5",
email: "david@example.com",
firstName: "David",
lastName: "Brown",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "inactive",
lastSignedIn: "2024-02-15",
},
{
id: "6",
email: "emily@example.com",
firstName: "Emily",
lastName: "Davis",
avatar: "/placeholder.svg?height=40&width=40",
role: "admin",
status: "active",
lastSignedIn: "2024-02-23",
},
{
id: "7",
email: "robert@example.com",
firstName: "Robert",
lastName: "Miller",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "active",
lastSignedIn: "2024-02-22",
},
{
id: "8",
email: "amanda@example.com",
firstName: "Amanda",
lastName: "Wilson",
avatar: "/placeholder.svg?height=40&width=40",
role: "staff",
status: "inactive",
lastSignedIn: "2024-02-18",
},
{
id: "9",
email: "thomas@example.com",
firstName: "Thomas",
lastName: "Moore",
avatar: "/placeholder.svg?height=40&width=40",
role: "user",
status: "active",
lastSignedIn: "2024-02-21",
},
{
id: "10",
email: "laura@example.com",
firstName: "Laura",
lastName: "Taylor",
avatar: "/placeholder.svg?height=40&width=40",
role: "staff",
status: "active",
lastSignedIn: "2024-02-19",
},
];
export type User = {
id: string;
email: string;
firstName: string;
lastName: string;
avatar: string;
role: "admin" | "staff" | "user";
status: "active" | "inactive";
lastSignedIn: string;
};
export function UsersTable() {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const [isSheetOpen, setIsSheetOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [searchValue, setSearchValue] = useState("");
// Apply search filter when search value changes
useEffect(() => {
if (searchValue) {
setColumnFilters((prev) => {
// Remove existing kolomusers filter if it exists
const filtered = prev.filter((filter) => filter.id !== "kolomusers");
// Add the new search filter
return [...filtered, { id: "kolomusers", value: searchValue }];
});
} else {
// Remove the filter if search is empty
setColumnFilters((prev) =>
prev.filter((filter) => filter.id !== "kolomusers")
);
}
}, [searchValue]);
const openEditSheet = useCallback((user: User) => {
setSelectedUser(user);
setIsSheetOpen(true);
}, []);
const handleUserUpdate = (updatedUser: User) => {
// Here you would typically update the user in your data source
console.log("Updated user:", updatedUser);
// For this example, we'll just update the local state
setSelectedUser(updatedUser);
};
const handleRoleFilter = (role: string) => {
if (role === "all") {
setColumnFilters((prev) => prev.filter((filter) => filter.id !== "role"));
} else {
setColumnFilters((prev) => {
const filtered = prev.filter((filter) => filter.id !== "role");
return [...filtered, { id: "role", value: role }];
});
}
};
const handleStatusFilter = (status: string) => {
if (status === "all") {
setColumnFilters((prev) =>
prev.filter((filter) => filter.id !== "status")
);
} else {
setColumnFilters((prev) => {
const filtered = prev.filter((filter) => filter.id !== "status");
return [...filtered, { id: "status", value: status }];
});
}
};
const columns: 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: "kolomusers",
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>
);
},
},
];
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
initialState: {
pagination: {
pageSize: 5,
},
},
});
return (
<div className="w-full">
<div className="flex items-center justify-between py-4">
<div className="flex items-center">
<Input
placeholder="Search users..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add User
</Button>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between space-x-2 py-4">
<div className="flex items-center gap-2">
<Select
value={table.getState().pagination.pageSize.toString()}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="w-32">
<SelectValue placeholder="Rows per page" />
</SelectTrigger>
<SelectContent>
{[5, 10, 20, 50].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize} rows
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</span>
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
<EditUserSheet
isOpen={isSheetOpen}
onOpenChange={setIsSheetOpen}
selectedUser={selectedUser}
onUserUpdate={handleUserUpdate}
/>
</div>
);
}

View File

@ -1,22 +1,9 @@
import { MapboxMap } from "@/components/map/mapbox-view";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { MapboxMap } from "@/app/protected/(admin-pages)/_components/map/mapbox-view";
export default function CrimeOverview() {
export default function CrimeOverviewPage() {
return (
<>
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 mb-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min">
<MapboxMap />

View File

@ -5,36 +5,18 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
} from "@/app/_components/ui/breadcrumb";
import { Separator } from "@/app/_components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
} from "@/app/_components/ui/sidebar";
export default function Dashboard() {
export default function DashboardPage() {
return (
<>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 mb-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />

View File

@ -1,5 +1,5 @@
import type React from "react";
import { AppSidebar } from "@/components/app-sidebar";
import { AppSidebar } from "@/app/protected/(admin-pages)/_components/app-sidebar";
import {
Breadcrumb,
BreadcrumbItem,
@ -7,24 +7,24 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
} from "@/app/_components/ui/breadcrumb";
import { Button } from "@/app/_components/ui/button";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
} from "@/app/_components/ui/sidebar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from "@/app/_components/ui/dropdown-menu";
import { MoreHorizontal } from "lucide-react";
import { InboxDrawer } from "@/components/inbox-drawer";
import { ThemeSwitcher } from "@/components/theme-switcher";
import FloatingActionSearchBar from "@/components/floating-action-search-bar";
import { Separator } from "@/components/ui/separator";
import { InboxDrawer } from "@/app/_components/inbox-drawer";
import { ThemeSwitcher } from "@/app/_components/theme-switcher";
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
import { Separator } from "@/app/_components/ui/separator";
export default async function Layout({
children,
@ -74,7 +74,6 @@ export default async function Layout({
</nav>
{/* Header with other controls */}
<header className="flex h-12 shrink-0 items-center justify-end border-b px-4 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8"></header>
<FloatingActionSearchBar />
{children}
</SidebarInset>

View File

@ -0,0 +1,27 @@
import { Button } from "@/app/_components/ui/button";
import { Plus } from "lucide-react";
import { UserStats } from "../../_components/users/user-stats";
import { UsersTable } from "../../_components/users/users-table";
export default function UsersPage() {
return (
<>
<header className="flex h-16 shrink-0 items-center justify-between px-4 mb-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-8">
<div>
<h1 className="text-lg font-semibold">User Management</h1>
<p className="text-sm text-muted-foreground">
Manage user accounts and permissions
</p>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<UserStats />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-background border md:min-h-min p-4">
<UsersTable />
</div>
</div>
</>
);
}

View File

@ -1,4 +1,4 @@
import FetchDataSteps from "@/components/tutorial/fetch-data-steps";
import FetchDataSteps from "@/app/_components/tutorial/fetch-data-steps";
import { createClient } from "@/utils/supabase/server";
import { InfoIcon } from "lucide-react";
import { redirect } from "next/navigation";

View File

@ -1,8 +1,9 @@
import { resetPasswordAction } from "@/actions/auth/reset-password";
import { FormMessage, Message } from "@/components/form-message";
import { SubmitButton } from "@/components/submit-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { resetPasswordAction } from "@/app/(auth-pages)/actions";
import { FormMessage, Message } from "@/app/_components/form-message";
import { SubmitButton } from "@/app/_components/submit-button";
import { Input } from "@/app/_components/ui/input";
import { Label } from "@/app/_components/ui/label";
export default async function ResetPassword(props: {
searchParams: Promise<Message>;

Some files were not shown because too many files have changed in this diff Show More