add users table
This commit is contained in:
parent
3ba462d797
commit
39a0af5e0f
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
);
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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.",
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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",
|
||||
// };
|
||||
// }
|
||||
// }
|
|
@ -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 {
|
|
@ -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({
|
|
@ -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,
|
|
@ -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";
|
|
@ -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({
|
|
@ -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, {
|
|
@ -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");
|
||||
};
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -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> = ({
|
|
@ -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";
|
||||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
))}
|
|
@ -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
|
||||
|
|
@ -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 }
|
|
@ -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,
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
}
|
|
@ -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();
|
|
@ -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
|
|
@ -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">
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
|
@ -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,
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue