add users table
This commit is contained in:
parent
3ba462d797
commit
39a0af5e0f
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
"deno.enablePaths": [
|
"deno.enablePaths": ["supabase/functions"],
|
||||||
"supabase/functions"
|
|
||||||
],
|
|
||||||
"deno.lint": true,
|
"deno.lint": true,
|
||||||
"deno.unstable": [
|
"deno.unstable": [
|
||||||
"bare-node-builtins",
|
"bare-node-builtins",
|
||||||
|
@ -19,6 +17,6 @@
|
||||||
"net"
|
"net"
|
||||||
],
|
],
|
||||||
"[typescript]": {
|
"[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,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/app/_components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/app/_components/ui/select";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../../../_components/ui/textarea";
|
||||||
import { SubmitButton } from "../submit-button";
|
import { SubmitButton } from "../../../_components/submit-button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TValidator } from "@/utils/validator";
|
import { TValidator } from "@/utils/validator";
|
||||||
import { useContactForm } from "@/src/infrastructure/hooks/use-contact-us-form";
|
import { FormField } from "../../../_components/form-field";
|
||||||
import { FormField } from "../form-field";
|
import { typeMessage } from "@/src/entities/models/contact-us.model";
|
||||||
import { typeMessage } from "@/src/applications/entities/models/contact-us.model";
|
import { Form } from "../../../_components/ui/form";
|
||||||
import { Form } from "../ui/form";
|
import { useContactForm } from "@/hooks/use-contact-us-form";
|
||||||
|
|
||||||
export function ContactUsForm() {
|
export function ContactUsForm() {
|
||||||
const {
|
const {
|
|
@ -5,8 +5,8 @@ import { useRouter } from "next/router";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/app/_hooks/use-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -15,8 +15,8 @@ import {
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/app/_components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
@ -24,9 +24,9 @@ import {
|
||||||
CardFooter,
|
CardFooter,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/app/_components/ui/card";
|
||||||
import { SubmitButton } from "@/components/submit-button";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { SubmitButton } from "@/app/_components/submit-button";
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
email: z.string().email({
|
email: z.string().email({
|
|
@ -1,14 +1,14 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
|
|
||||||
import { Github, Lock } from "lucide-react";
|
import { Github, Lock } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SubmitButton } from "../submit-button";
|
import { SubmitButton } from "../../../_components/submit-button";
|
||||||
|
|
||||||
import { FormField } from "../form-field";
|
import { FormField } from "../../../_components/form-field";
|
||||||
import { useSignInForm } from "@/src/infrastructure/hooks/use-signin";
|
import { useSignInForm } from "@/hooks/use-signin";
|
||||||
|
|
||||||
export function LoginForm2({
|
export function LoginForm2({
|
||||||
className,
|
className,
|
|
@ -1,13 +1,13 @@
|
||||||
// import { cn } from "@/lib/utils";
|
// import { cn } from "@/lib/utils";
|
||||||
// import { Button } from "@/components/ui/button";
|
// import { Button } from "@/app/_components/ui/button";
|
||||||
// import {
|
// import {
|
||||||
// Card,
|
// Card,
|
||||||
// CardContent,
|
// CardContent,
|
||||||
// CardDescription,
|
// CardDescription,
|
||||||
// CardHeader,
|
// CardHeader,
|
||||||
// CardTitle,
|
// CardTitle,
|
||||||
// } from "@/components/ui/card";
|
// } from "@/app/_components/ui/card";
|
||||||
// import { Input } from "@/components/ui/input";
|
// import { Input } from "@/app/_components/ui/input";
|
||||||
// import { Label } from "@/components/ui/label";
|
// import { Label } from "@/components/ui/label";
|
||||||
// import { SubmitButton } from "../submit-button";
|
// import { SubmitButton } from "../submit-button";
|
||||||
// import { signInAction } from "@/actions/auth/sign-in";
|
// import { signInAction } from "@/actions/auth/sign-in";
|
|
@ -4,8 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/app/_hooks/use-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -14,20 +14,20 @@ import {
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/app/_components/ui/form";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/app/_components/ui/input-otp";
|
||||||
import { SubmitButton } from "../submit-button";
|
import { SubmitButton } from "../../../_components/submit-button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../ui/card";
|
} from "../../../_components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
|
@ -3,7 +3,7 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/app/_hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -11,24 +11,24 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/app/_components/ui/form";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/app/_components/ui/input-otp";
|
||||||
import { SubmitButton } from "../submit-button";
|
import { SubmitButton } from "../../../_components/submit-button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "../ui/card";
|
} from "../../../_components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { verifyOtpAction } from "@/actions/auth/verify-otp";
|
import { verifyOtpAction } from "@/actions/auth/verify-otp";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../../../_components/ui/input";
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
token: z.string().min(6, {
|
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 { ContactUsForm } from "@/app/(auth-pages)/_components/auth/contact-us";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { GalleryVerticalEnd, Globe } from "lucide-react";
|
import { GalleryVerticalEnd, Globe } from "lucide-react";
|
||||||
|
|
||||||
export default async function ContactAdminPage() {
|
export default async function ContactAdminPage() {
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import RecoveryEmailForm from "@/components/auth/email-recovery";
|
import RecoveryEmailForm from "@/app/(auth-pages)/_components/auth/email-recovery";
|
||||||
import { VerifyOtpForm } from "@/components/auth/verify-otp-form";
|
|
||||||
|
|
||||||
import { GalleryVerticalEnd } from "lucide-react";
|
import { GalleryVerticalEnd } from "lucide-react";
|
||||||
|
|
||||||
export default async function VerifyOtpPage() {
|
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 Link from "next/link";
|
||||||
import { SmtpMessage } from "../smtp-message";
|
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: {
|
export default async function ForgotPassword(props: {
|
||||||
searchParams: Promise<Message>;
|
searchParams: Promise<Message>;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { checkSession } from "@/actions/auth/session";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { checkSession } from "./actions";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
children,
|
children,
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { FormMessage, Message } from "@/components/form-message";
|
import { Message } from "@/app/_components/form-message";
|
||||||
import { SubmitButton } from "@/components/submit-button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { GalleryVerticalEnd, Globe } from "lucide-react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { LoginForm2 } from "@/app/(auth-pages)/_components/auth/login-form-2";
|
||||||
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";
|
|
||||||
|
|
||||||
export default async function Login(props: { searchParams: Promise<Message> }) {
|
export default async function Login(props: { searchParams: Promise<Message> }) {
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormMessage, Message } from "@/components/form-message";
|
import { FormMessage, Message } from "@/app/_components/form-message";
|
||||||
import { SubmitButton } from "@/components/submit-button";
|
import { SubmitButton } from "@/app/_components/submit-button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/app/_components/ui/label";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SmtpMessage } from "../smtp-message";
|
import { SmtpMessage } from "../smtp-message";
|
||||||
import { signUpAction } from "@/actions/auth/sign-up-action";
|
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";
|
import { GalleryVerticalEnd } from "lucide-react";
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,35 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react"
|
import React, {
|
||||||
import { Input } from "@/components/ui/input"
|
useState,
|
||||||
import { motion, AnimatePresence } from "framer-motion"
|
useEffect,
|
||||||
import { Search, Send, BarChart2, Globe, Video, PlaneTakeoff, AudioLines } from "lucide-react"
|
forwardRef,
|
||||||
import useDebounce from "@/hooks/use-debounce"
|
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 {
|
interface Action {
|
||||||
id: string
|
id: string;
|
||||||
label: string
|
label: string;
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode;
|
||||||
description?: string
|
description?: string;
|
||||||
short?: string
|
short?: string;
|
||||||
end?: string
|
end?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
actions: Action[]
|
actions: Action[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allActions = [
|
const allActions = [
|
||||||
|
@ -60,49 +73,49 @@ const allActions = [
|
||||||
short: "",
|
short: "",
|
||||||
end: "Command",
|
end: "Command",
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
interface ActionSearchBarProps {
|
interface ActionSearchBarProps {
|
||||||
actions?: Action[]
|
actions?: Action[];
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean;
|
||||||
isFloating?: boolean
|
isFloating?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
({ actions = allActions, autoFocus = false, isFloating = false }, ref) => {
|
({ actions = allActions, autoFocus = false, isFloating = false }, ref) => {
|
||||||
const [query, setQuery] = useState("")
|
const [query, setQuery] = useState("");
|
||||||
const [result, setResult] = useState<SearchResult | null>(null)
|
const [result, setResult] = useState<SearchResult | null>(null);
|
||||||
const [isFocused, setIsFocused] = useState(autoFocus)
|
const [isFocused, setIsFocused] = useState(autoFocus);
|
||||||
const [selectedAction, setSelectedAction] = useState<Action | null>(null)
|
const [selectedAction, setSelectedAction] = useState<Action | null>(null);
|
||||||
const debouncedQuery = useDebounce(query, 200)
|
const debouncedQuery = useDebounce(query, 200);
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
|
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus && inputRef.current) {
|
if (autoFocus && inputRef.current) {
|
||||||
inputRef.current.focus()
|
inputRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [autoFocus])
|
}, [autoFocus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!debouncedQuery) {
|
if (!debouncedQuery) {
|
||||||
setResult({ actions: allActions })
|
setResult({ actions: allActions });
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedQuery = debouncedQuery.toLowerCase().trim()
|
const normalizedQuery = debouncedQuery.toLowerCase().trim();
|
||||||
const filteredActions = allActions.filter((action) => {
|
const filteredActions = allActions.filter((action) => {
|
||||||
const searchableText = action.label.toLowerCase()
|
const searchableText = action.label.toLowerCase();
|
||||||
return searchableText.includes(normalizedQuery)
|
return searchableText.includes(normalizedQuery);
|
||||||
})
|
});
|
||||||
|
|
||||||
setResult({ actions: filteredActions })
|
setResult({ actions: filteredActions });
|
||||||
}, [debouncedQuery])
|
}, [debouncedQuery]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setQuery(e.target.value)
|
setQuery(e.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const container = {
|
const container = {
|
||||||
hidden: { opacity: 0, height: 0 },
|
hidden: { opacity: 0, height: 0 },
|
||||||
|
@ -128,7 +141,7 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
hidden: { opacity: 0, y: 20 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
@ -146,10 +159,12 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@ -158,7 +173,9 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
value={query}
|
value={query}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onFocus={() => setIsFocused(true)}
|
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"
|
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">
|
<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 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-gray-500">{action.icon}</span>
|
<span className="text-gray-500">{action.icon}</span>
|
||||||
<span className="text-sm font-medium">{action.label}</span>
|
<span className="text-sm font-medium">
|
||||||
<span className="text-xs text-muted-foreground">{action.description}</span>
|
{action.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{action.description}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-muted-foreground">{action.short}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
<span className="text-xs text-muted-foreground text-right">{action.end}</span>
|
{action.short}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground text-right">
|
||||||
|
{action.end}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
|
@ -230,11 +255,10 @@ const ActionSearchBar = forwardRef<HTMLInputElement, ActionSearchBarProps>(
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
ActionSearchBar.displayName = "ActionSearchBar"
|
ActionSearchBar.displayName = "ActionSearchBar";
|
||||||
|
|
||||||
export default ActionSearchBar
|
|
||||||
|
|
||||||
|
export default ActionSearchBar;
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
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() {
|
export default function FloatingActionSearchBar() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
|
@ -2,18 +2,22 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Inbox, Search, ArrowLeft } from "lucide-react";
|
import { Inbox, Search, ArrowLeft } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/app/_components/ui/sheet";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/app/_components/ui/badge";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from "@/app/_components/ui/avatar";
|
||||||
|
|
||||||
interface InboxDrawerProps {
|
interface InboxDrawerProps {
|
||||||
showTitle?: boolean;
|
showTitle?: boolean;
|
||||||
|
@ -95,7 +99,6 @@ const sampleMails: MailMessage[] = [
|
||||||
date: "4 days ago",
|
date: "4 days ago",
|
||||||
read: true,
|
read: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
|
const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
|
||||||
|
@ -183,19 +186,19 @@ const InboxDrawerComponent: React.FC<InboxDrawerProps> = ({
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* {showAvatar && ( */}
|
{/* {showAvatar && ( */}
|
||||||
<Avatar className="w-10 h-10">
|
<Avatar className="w-10 h-10">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
src={`https://api.dicebear.com/6.x/initials/svg?seed=${selectedMessage.name}`}
|
src={`https://api.dicebear.com/6.x/initials/svg?seed=${selectedMessage.name}`}
|
||||||
alt={selectedMessage.name}
|
alt={selectedMessage.name}
|
||||||
/>
|
/>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{selectedMessage.name
|
{selectedMessage.name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((n) => n[0])
|
.map((n) => n[0])
|
||||||
.join("")
|
.join("")
|
||||||
.toUpperCase()}
|
.toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{selectedMessage.name}</p>
|
<p className="font-medium">{selectedMessage.name}</p>
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { type ComponentProps } from "react";
|
import { type ComponentProps } from "react";
|
||||||
import { useFormStatus } from "react-dom";
|
import { useFormStatus } from "react-dom";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { ChevronsUpDown, Plus } from "lucide-react"
|
import { ChevronsUpDown, Plus } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -11,25 +11,25 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/app/_components/ui/sidebar";
|
||||||
|
|
||||||
export function TeamSwitcher({
|
export function TeamSwitcher({
|
||||||
teams,
|
teams,
|
||||||
}: {
|
}: {
|
||||||
teams: {
|
teams: {
|
||||||
name: string
|
name: string;
|
||||||
logo: React.ElementType
|
logo: React.ElementType;
|
||||||
plan: string
|
plan: string;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
const [activeTeam, setActiveTeam] = React.useState(teams[0])
|
const [activeTeam, setActiveTeam] = React.useState(teams[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
@ -85,5 +85,5 @@ export function TeamSwitcher({
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react";
|
import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
@ -78,7 +78,7 @@ const ThemeSwitcherComponent = ({
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
value={option.value}
|
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>
|
<span>{option.label}</span>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
))}
|
))}
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from "react-hook-form"
|
} from "react-hook-form"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/app/_components/ui/label"
|
||||||
|
|
||||||
const Form = FormProvider
|
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 * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { VariantProps, cva } from "class-variance-authority"
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
import { PanelLeft } from "lucide-react"
|
import { PanelLeft } from "lucide-react";
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/app/_hooks/use-mobile";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
import { Sheet, SheetContent } from "@/app/_components/ui/sheet";
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/app/_components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/app/_components/ui/tooltip";
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
const SIDEBAR_WIDTH = "16rem";
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||||
|
|
||||||
type SidebarContext = {
|
type SidebarContext = {
|
||||||
state: "expanded" | "collapsed"
|
state: "expanded" | "collapsed";
|
||||||
open: boolean
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void;
|
||||||
openMobile: boolean
|
openMobile: boolean;
|
||||||
setOpenMobile: (open: boolean) => void
|
setOpenMobile: (open: boolean) => void;
|
||||||
isMobile: boolean
|
isMobile: boolean;
|
||||||
toggleSidebar: () => void
|
toggleSidebar: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContext | null>(null)
|
const SidebarContext = React.createContext<SidebarContext | null>(null);
|
||||||
|
|
||||||
function useSidebar() {
|
function useSidebar() {
|
||||||
const context = React.useContext(SidebarContext)
|
const context = React.useContext(SidebarContext);
|
||||||
if (!context) {
|
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<
|
const SidebarProvider = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean;
|
||||||
open?: boolean
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
|
@ -67,34 +67,34 @@ const SidebarProvider = React.forwardRef<
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile();
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
const [openMobile, setOpenMobile] = React.useState(false);
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
// This is the internal state of the sidebar.
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
// We use openProp and setOpenProp for control from outside the component.
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||||
const open = openProp ?? _open
|
const open = openProp ?? _open;
|
||||||
const setOpen = React.useCallback(
|
const setOpen = React.useCallback(
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
(value: boolean | ((value: boolean) => boolean)) => {
|
||||||
const openState = typeof value === "function" ? value(open) : value
|
const openState = typeof value === "function" ? value(open) : value;
|
||||||
if (setOpenProp) {
|
if (setOpenProp) {
|
||||||
setOpenProp(openState)
|
setOpenProp(openState);
|
||||||
} else {
|
} else {
|
||||||
_setOpen(openState)
|
_setOpen(openState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
// 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]
|
[setOpenProp, open]
|
||||||
)
|
);
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
// Helper to toggle the sidebar.
|
||||||
const toggleSidebar = React.useCallback(() => {
|
const toggleSidebar = React.useCallback(() => {
|
||||||
return isMobile
|
return isMobile
|
||||||
? setOpenMobile((open) => !open)
|
? setOpenMobile((open) => !open)
|
||||||
: setOpen((open) => !open)
|
: setOpen((open) => !open);
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
}, [isMobile, setOpen, setOpenMobile]);
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
// Adds a keyboard shortcut to toggle the sidebar.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -103,18 +103,18 @@ const SidebarProvider = React.forwardRef<
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||||
(event.metaKey || event.ctrlKey)
|
(event.metaKey || event.ctrlKey)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
toggleSidebar()
|
toggleSidebar();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [toggleSidebar])
|
}, [toggleSidebar]);
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
// 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.
|
// 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>(
|
const contextValue = React.useMemo<SidebarContext>(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -127,7 +127,7 @@ const SidebarProvider = React.forwardRef<
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
}),
|
}),
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext.Provider value={contextValue}>
|
||||||
|
@ -151,17 +151,17 @@ const SidebarProvider = React.forwardRef<
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
SidebarProvider.displayName = "SidebarProvider"
|
SidebarProvider.displayName = "SidebarProvider";
|
||||||
|
|
||||||
const Sidebar = React.forwardRef<
|
const Sidebar = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
side?: "left" | "right"
|
side?: "left" | "right";
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
variant?: "sidebar" | "floating" | "inset";
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
collapsible?: "offcanvas" | "icon" | "none";
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
|
@ -175,7 +175,7 @@ const Sidebar = React.forwardRef<
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
if (collapsible === "none") {
|
if (collapsible === "none") {
|
||||||
return (
|
return (
|
||||||
|
@ -189,7 +189,7 @@ const Sidebar = React.forwardRef<
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
@ -209,7 +209,7 @@ const Sidebar = React.forwardRef<
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -254,16 +254,16 @@ const Sidebar = React.forwardRef<
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
Sidebar.displayName = "Sidebar"
|
Sidebar.displayName = "Sidebar";
|
||||||
|
|
||||||
const SidebarTrigger = React.forwardRef<
|
const SidebarTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof Button>,
|
React.ElementRef<typeof Button>,
|
||||||
React.ComponentProps<typeof Button>
|
React.ComponentProps<typeof Button>
|
||||||
>(({ className, onClick, ...props }, ref) => {
|
>(({ className, onClick, ...props }, ref) => {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -273,23 +273,23 @@ const SidebarTrigger = React.forwardRef<
|
||||||
size="icon"
|
size="icon"
|
||||||
className={cn("h-7 w-7", className)}
|
className={cn("h-7 w-7", className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onClick?.(event)
|
onClick?.(event);
|
||||||
toggleSidebar()
|
toggleSidebar();
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarTrigger.displayName = "SidebarTrigger"
|
SidebarTrigger.displayName = "SidebarTrigger";
|
||||||
|
|
||||||
const SidebarRail = React.forwardRef<
|
const SidebarRail = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button">
|
React.ComponentProps<"button">
|
||||||
>(({ className, ...props }, ref) => {
|
>(({ className, ...props }, ref) => {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -310,9 +310,9 @@ const SidebarRail = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarRail.displayName = "SidebarRail"
|
SidebarRail.displayName = "SidebarRail";
|
||||||
|
|
||||||
const SidebarInset = React.forwardRef<
|
const SidebarInset = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -328,9 +328,9 @@ const SidebarInset = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarInset.displayName = "SidebarInset"
|
SidebarInset.displayName = "SidebarInset";
|
||||||
|
|
||||||
const SidebarInput = React.forwardRef<
|
const SidebarInput = React.forwardRef<
|
||||||
React.ElementRef<typeof Input>,
|
React.ElementRef<typeof Input>,
|
||||||
|
@ -346,9 +346,9 @@ const SidebarInput = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarInput.displayName = "SidebarInput"
|
SidebarInput.displayName = "SidebarInput";
|
||||||
|
|
||||||
const SidebarHeader = React.forwardRef<
|
const SidebarHeader = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -361,9 +361,9 @@ const SidebarHeader = React.forwardRef<
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarHeader.displayName = "SidebarHeader"
|
SidebarHeader.displayName = "SidebarHeader";
|
||||||
|
|
||||||
const SidebarFooter = React.forwardRef<
|
const SidebarFooter = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -376,9 +376,9 @@ const SidebarFooter = React.forwardRef<
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarFooter.displayName = "SidebarFooter"
|
SidebarFooter.displayName = "SidebarFooter";
|
||||||
|
|
||||||
const SidebarSeparator = React.forwardRef<
|
const SidebarSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof Separator>,
|
React.ElementRef<typeof Separator>,
|
||||||
|
@ -391,9 +391,9 @@ const SidebarSeparator = React.forwardRef<
|
||||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarSeparator.displayName = "SidebarSeparator"
|
SidebarSeparator.displayName = "SidebarSeparator";
|
||||||
|
|
||||||
const SidebarContent = React.forwardRef<
|
const SidebarContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -409,9 +409,9 @@ const SidebarContent = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarContent.displayName = "SidebarContent"
|
SidebarContent.displayName = "SidebarContent";
|
||||||
|
|
||||||
const SidebarGroup = React.forwardRef<
|
const SidebarGroup = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -424,15 +424,15 @@ const SidebarGroup = React.forwardRef<
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroup.displayName = "SidebarGroup"
|
SidebarGroup.displayName = "SidebarGroup";
|
||||||
|
|
||||||
const SidebarGroupLabel = React.forwardRef<
|
const SidebarGroupLabel = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "div"
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
@ -445,15 +445,15 @@ const SidebarGroupLabel = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||||
|
|
||||||
const SidebarGroupAction = React.forwardRef<
|
const SidebarGroupAction = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||||
>(({ className, asChild = false, ...props }, ref) => {
|
>(({ className, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
@ -468,9 +468,9 @@ const SidebarGroupAction = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||||
|
|
||||||
const SidebarGroupContent = React.forwardRef<
|
const SidebarGroupContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -482,8 +482,8 @@ const SidebarGroupContent = React.forwardRef<
|
||||||
className={cn("w-full text-sm", className)}
|
className={cn("w-full text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||||
|
|
||||||
const SidebarMenu = React.forwardRef<
|
const SidebarMenu = React.forwardRef<
|
||||||
HTMLUListElement,
|
HTMLUListElement,
|
||||||
|
@ -495,8 +495,8 @@ const SidebarMenu = React.forwardRef<
|
||||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenu.displayName = "SidebarMenu"
|
SidebarMenu.displayName = "SidebarMenu";
|
||||||
|
|
||||||
const SidebarMenuItem = React.forwardRef<
|
const SidebarMenuItem = React.forwardRef<
|
||||||
HTMLLIElement,
|
HTMLLIElement,
|
||||||
|
@ -508,8 +508,8 @@ const SidebarMenuItem = React.forwardRef<
|
||||||
className={cn("group/menu-item relative", className)}
|
className={cn("group/menu-item relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
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",
|
"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",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
const SidebarMenuButton = React.forwardRef<
|
const SidebarMenuButton = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & {
|
React.ComponentProps<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
|
@ -553,8 +553,8 @@ const SidebarMenuButton = React.forwardRef<
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
const { isMobile, state } = useSidebar()
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Comp
|
<Comp
|
||||||
|
@ -565,16 +565,16 @@ const SidebarMenuButton = React.forwardRef<
|
||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
|
|
||||||
if (!tooltip) {
|
if (!tooltip) {
|
||||||
return button
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
if (typeof tooltip === "string") {
|
||||||
tooltip = {
|
tooltip = {
|
||||||
children: tooltip,
|
children: tooltip,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -587,19 +587,19 @@ const SidebarMenuButton = React.forwardRef<
|
||||||
{...tooltip}
|
{...tooltip}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||||
|
|
||||||
const SidebarMenuAction = React.forwardRef<
|
const SidebarMenuAction = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> & {
|
React.ComponentProps<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
showOnHover?: boolean
|
showOnHover?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
@ -619,9 +619,9 @@ const SidebarMenuAction = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||||
|
|
||||||
const SidebarMenuBadge = React.forwardRef<
|
const SidebarMenuBadge = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
|
@ -641,19 +641,19 @@ const SidebarMenuBadge = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||||
|
|
||||||
const SidebarMenuSkeleton = React.forwardRef<
|
const SidebarMenuSkeleton = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
showIcon?: boolean
|
showIcon?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, showIcon = false, ...props }, ref) => {
|
>(({ className, showIcon = false, ...props }, ref) => {
|
||||||
// Random width between 50 to 90%.
|
// Random width between 50 to 90%.
|
||||||
const width = React.useMemo(() => {
|
const width = React.useMemo(() => {
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -678,9 +678,9 @@ const SidebarMenuSkeleton = React.forwardRef<
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||||
|
|
||||||
const SidebarMenuSub = React.forwardRef<
|
const SidebarMenuSub = React.forwardRef<
|
||||||
HTMLUListElement,
|
HTMLUListElement,
|
||||||
|
@ -696,24 +696,24 @@ const SidebarMenuSub = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||||
|
|
||||||
const SidebarMenuSubItem = React.forwardRef<
|
const SidebarMenuSubItem = React.forwardRef<
|
||||||
HTMLLIElement,
|
HTMLLIElement,
|
||||||
React.ComponentProps<"li">
|
React.ComponentProps<"li">
|
||||||
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||||
|
|
||||||
const SidebarMenuSubButton = React.forwardRef<
|
const SidebarMenuSubButton = React.forwardRef<
|
||||||
HTMLAnchorElement,
|
HTMLAnchorElement,
|
||||||
React.ComponentProps<"a"> & {
|
React.ComponentProps<"a"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
size?: "sm" | "md"
|
size?: "sm" | "md";
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "a"
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
@ -731,9 +731,9 @@ const SidebarMenuSubButton = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
@ -760,4 +760,4 @@ export {
|
||||||
SidebarSeparator,
|
SidebarSeparator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
useSidebar,
|
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";
|
"use client";
|
||||||
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/app/_hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Toast,
|
Toast,
|
||||||
ToastClose,
|
ToastClose,
|
||||||
|
@ -8,7 +8,7 @@ import {
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
ToastViewport,
|
ToastViewport,
|
||||||
} from "@/components/ui/toast";
|
} from "@/app/_components/ui/toast";
|
||||||
|
|
||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
const { toasts } = useToast();
|
const { toasts } = useToast();
|
|
@ -6,7 +6,7 @@ import * as React from "react"
|
||||||
import type {
|
import type {
|
||||||
ToastActionElement,
|
ToastActionElement,
|
||||||
ToastProps,
|
ToastProps,
|
||||||
} from "@/components/ui/toast"
|
} from "@/app/_components/ui/toast"
|
||||||
|
|
||||||
const TOAST_LIMIT = 1
|
const TOAST_LIMIT = 1
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
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 { hasEnvVars } from "@/utils/supabase/check-env-vars";
|
||||||
import { Geist } from "next/font/google";
|
import { Geist } from "next/font/google";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import SupabaseLogo from "@/components/logo/supabase-logo";
|
import { Toaster } from "./_components/ui/toaster";
|
||||||
import SigapLogo from "@/components/logo/sigap-logo";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
|
||||||
|
|
||||||
const defaultUrl = process.env.VERCEL_URL
|
const defaultUrl = process.env.VERCEL_URL
|
||||||
? `https://${process.env.VERCEL_URL}`
|
? `https://${process.env.VERCEL_URL}`
|
||||||
|
@ -17,8 +11,8 @@ const defaultUrl = process.env.VERCEL_URL
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
metadataBase: new URL(defaultUrl),
|
metadataBase: new URL(defaultUrl),
|
||||||
title: "Next.js and Supabase Starter Kit",
|
title: "Sigap",
|
||||||
description: "The fastest way to build apps with Next.js and Supabase",
|
description: "Sigap is a platform for managing your data.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
@ -31,7 +25,6 @@ export default function RootLayout({
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={geistSans.className} suppressHydrationWarning>
|
<html lang="en" className={geistSans.className} suppressHydrationWarning>
|
||||||
<body className="bg-background text-foreground">
|
<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 { 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() {
|
export default async function Home() {
|
||||||
return (
|
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,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/app/_components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
|
@ -14,10 +14,10 @@ import {
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/app/_components/ui/sidebar";
|
||||||
|
|
||||||
import * as TablerIcons from "@tabler/icons-react";
|
import * as TablerIcons from "@tabler/icons-react";
|
||||||
import { useNavigations } from "@/hooks/use-navigations";
|
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||||
|
|
||||||
interface SubSubItem {
|
interface SubSubItem {
|
||||||
title: string;
|
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 {
|
import {
|
||||||
Folder,
|
Folder,
|
||||||
|
@ -6,7 +6,7 @@ import {
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
Trash2,
|
Trash2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -14,7 +14,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
|
@ -23,7 +23,7 @@ import {
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/app/_components/ui/sidebar";
|
||||||
|
|
||||||
import * as TablerIcons from "@tabler/icons-react";
|
import * as TablerIcons from "@tabler/icons-react";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
BadgeCheck,
|
||||||
|
@ -7,13 +7,13 @@ import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
LogOut,
|
LogOut,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from "@/components/ui/avatar"
|
} from "@/app/_components/ui/avatar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
@ -22,13 +22,13 @@ import {
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/app/_components/ui/sidebar";
|
||||||
import {
|
import {
|
||||||
IconBadgeCc,
|
IconBadgeCc,
|
||||||
IconBell,
|
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 { MapboxMap } from "@/app/protected/(admin-pages)/_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";
|
|
||||||
|
|
||||||
export default function CrimeOverview() {
|
export default function CrimeOverviewPage() {
|
||||||
return (
|
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="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">
|
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min">
|
||||||
<MapboxMap />
|
<MapboxMap />
|
||||||
|
|
|
@ -5,36 +5,18 @@ import {
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/app/_components/ui/breadcrumb";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/app/_components/ui/sidebar";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function DashboardPage() {
|
||||||
return (
|
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">
|
<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 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>
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
<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="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
<div className="aspect-video rounded-xl bg-muted/50" />
|
<div className="aspect-video rounded-xl bg-muted/50" />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { AppSidebar } from "@/components/app-sidebar";
|
import { AppSidebar } from "@/app/protected/(admin-pages)/_components/app-sidebar";
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
|
@ -7,24 +7,24 @@ import {
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/app/_components/ui/breadcrumb";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/app/_components/ui/sidebar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
import { InboxDrawer } from "@/components/inbox-drawer";
|
import { InboxDrawer } from "@/app/_components/inbox-drawer";
|
||||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
import { ThemeSwitcher } from "@/app/_components/theme-switcher";
|
||||||
import FloatingActionSearchBar from "@/components/floating-action-search-bar";
|
import FloatingActionSearchBar from "@/app/_components/floating-action-search-bar";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
|
|
||||||
export default async function Layout({
|
export default async function Layout({
|
||||||
children,
|
children,
|
||||||
|
@ -74,7 +74,6 @@ export default async function Layout({
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Header with other controls */}
|
{/* 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 />
|
<FloatingActionSearchBar />
|
||||||
{children}
|
{children}
|
||||||
</SidebarInset>
|
</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 { createClient } from "@/utils/supabase/server";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { resetPasswordAction } from "@/actions/auth/reset-password";
|
|
||||||
import { FormMessage, Message } from "@/components/form-message";
|
import { resetPasswordAction } from "@/app/(auth-pages)/actions";
|
||||||
import { SubmitButton } from "@/components/submit-button";
|
import { FormMessage, Message } from "@/app/_components/form-message";
|
||||||
import { Input } from "@/components/ui/input";
|
import { SubmitButton } from "@/app/_components/submit-button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
|
import { Label } from "@/app/_components/ui/label";
|
||||||
|
|
||||||
export default async function ResetPassword(props: {
|
export default async function ResetPassword(props: {
|
||||||
searchParams: Promise<Message>;
|
searchParams: Promise<Message>;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue