Added customer list, coupon list and started working on purchase list

This commit is contained in:
Thastertyn committed 2025-04-21 19:29:01 +02:00
1 parent 2fe5dbcb21
commit f2af9dc566
223 files changed
+5027 -3017

No files matched your search

+3 -2
View File
@@ -1,4 +1,4 @@
import {AdminAppName} from "@/config/manifest"
import { AdminAppName } from "@/config/manifest";
interface Props {
children: React.ReactNode;
@@ -17,7 +17,8 @@ export default function AuthLayout({ children }: Props) {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-6 w-6">
className="mr-2 h-6 w-6"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
<h1 className="text-xl font-medium">{AdminAppName}</h1>
@@ -10,7 +10,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -20,7 +20,7 @@ const formSchema = z.object({
email: z
.string()
.min(1, { message: "Please enter your email" })
.email({ message: "Invalid email address" })
.email({ message: "Invalid email address" }),
});
export function ForgotForm({ className, ...props }: ForgotFormProps) {
@@ -28,7 +28,7 @@ export function ForgotForm({ className, ...props }: ForgotFormProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "" }
defaultValues: { email: "" },
});
function onSubmit(data: z.infer<typeof formSchema>) {
@@ -21,7 +21,8 @@ export default function ForgotPassword() {
Don't have an account?{" "}
<Link
to="/sign-up"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Sign up
</Link>
.
@@ -11,7 +11,7 @@ import {
FormControl,
FormField,
FormItem,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
@@ -20,7 +20,7 @@ import { PinInput, PinInputField } from "@/components/pin-input";
type OtpFormProps = HTMLAttributes<HTMLDivElement>;
const formSchema = z.object({
otp: z.string().min(1, { message: "Please enter your otp code." })
otp: z.string().min(1, { message: "Please enter your otp code." }),
});
export function OtpForm({ className, ...props }: OtpFormProps) {
@@ -30,7 +30,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { otp: "" }
defaultValues: { otp: "" },
});
function onSubmit(data: z.infer<typeof formSchema>) {
@@ -41,7 +41,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
setTimeout(() => {
@@ -65,7 +65,8 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
{...field}
className="flex h-10 justify-between"
onComplete={() => setDisabledBtn(false)}
onIncomplete={() => setDisabledBtn(true)}>
onIncomplete={() => setDisabledBtn(true)}
>
{Array.from({ length: 7 }, (_, i) => {
if (i === 3)
return <Separator key={i} orientation="vertical" />;
+2 -1
View File
@@ -21,7 +21,8 @@ export default function Otp() {
Haven't received it?{" "}
<Link
to="/sign-in"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Resend a new code.
</Link>
.
@@ -11,7 +11,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/password-input";
@@ -27,11 +27,11 @@ const formSchema = z.object({
password: z
.string()
.min(1, {
message: "Please enter your password"
message: "Please enter your password",
})
.min(7, {
message: "Password must be at least 7 characters long"
})
message: "Password must be at least 7 characters long",
}),
});
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
@@ -41,14 +41,16 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: ""
}
password: "",
},
});
async function onSubmit(data: z.infer<typeof formSchema>) {
resetError();
try {
await loginMutation.mutateAsync({formData: {username: data.email, password: data.password}});
await loginMutation.mutateAsync({
formData: { username: data.email, password: data.password },
});
} catch {
// Error handled in useAuth hook
}
@@ -81,7 +83,8 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
<FormLabel>Password</FormLabel>
<Link
to="/forgot-password"
className="text-sm font-medium text-muted-foreground hover:opacity-75">
className="text-sm font-medium text-muted-foreground hover:opacity-75"
>
Forgot password?
</Link>
</div>
+4 -2
View File
@@ -18,13 +18,15 @@ export default function SignIn() {
By clicking login, you agree to our{" "}
<a
href="/terms"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</a>{" "}
and{" "}
<a
href="/privacy"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</a>
.
@@ -16,7 +16,8 @@ export default function SignIn2() {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-6 w-6">
className="mr-2 h-6 w-6"
>
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
</svg>
{AdminAppName}
@@ -55,13 +56,15 @@ export default function SignIn2() {
By clicking login, you agree to our{" "}
<a
href="/terms"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</a>{" "}
and{" "}
<a
href="/privacy"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</a>
.
@@ -10,7 +10,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/password-input";
@@ -41,27 +41,27 @@ const formSchema = z
.string()
.min(1, { message: "Please enter your password" })
.min(8, {
message: "Password must be at least 8 characters long"
message: "Password must be at least 8 characters long",
})
.max(128, { message: "Password must be at most 128 characters long" })
.refine((password) => /[A-Z]/.test(password), {
message: "Password must contain at least one uppercase letter"
message: "Password must contain at least one uppercase letter",
})
.refine((password) => /[a-z]/.test(password), {
message: "Password must contain at least one lowercase letter"
message: "Password must contain at least one lowercase letter",
})
.refine((password) => /\d/.test(password), {
message: "Password must contain at least one number"
message: "Password must contain at least one number",
})
.refine((password) => /[@$!%*?&]/.test(password), {
message:
"Password must contain at least one special character (@, $, !, %, *, ?, &)"
"Password must contain at least one special character (@, $, !, %, *, ?, &)",
}),
confirmPassword: z.string()
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match.",
path: ["confirmPassword"]
path: ["confirmPassword"],
});
export function SignUpForm({ className, ...props }: SignUpFormProps) {
@@ -75,15 +75,15 @@ export function SignUpForm({ className, ...props }: SignUpFormProps) {
email: "",
phone_number: "",
password: "",
confirmPassword: ""
}
confirmPassword: "",
},
});
async function onSubmit(data: z.infer<typeof formSchema>) {
setIsLoading(true);
signUpMutation.mutate(data);
toast({
title: "Successfully registered"
title: "Successfully registered",
});
setIsLoading(false);
}
+6 -3
View File
@@ -16,7 +16,8 @@ export default function SignUp() {
Already have an account?{" "}
<Link
to="/sign-in"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Sign In
</Link>
</p>
@@ -26,13 +27,15 @@ export default function SignUp() {
By creating an account, you agree to our{" "}
<a
href="/terms"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</a>{" "}
and{" "}
<a
href="/privacy"
className="underline underline-offset-4 hover:text-primary">
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</a>
.
@@ -9,13 +9,13 @@ import {
CommandGroup,
CommandInput,
CommandItem,
CommandList
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
import { ChatUser } from "../data/chat-types";
@@ -56,7 +56,7 @@ export function NewChat({ users, onOpenChange, open }: Props) {
{JSON.stringify(selectedUsers, null, 2)}
</code>
</pre>
)
),
});
};
@@ -79,7 +79,8 @@ export function NewChat({ users, onOpenChange, open }: Props) {
handleRemoveUser(user.id);
}
}}
onClick={() => handleRemoveUser(user.id)}>
onClick={() => handleRemoveUser(user.id)}
>
<IconX className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
@@ -97,7 +98,8 @@ export function NewChat({ users, onOpenChange, open }: Props) {
<CommandItem
key={user.id}
onSelect={() => handleSelectUser(user)}
className="flex items-center justify-between gap-2">
className="flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2">
<img
src={user.profile || "/placeholder.svg"}
@@ -125,7 +127,8 @@ export function NewChat({ users, onOpenChange, open }: Props) {
<Button
variant={"default"}
onClick={onSubmit}
disabled={selectedUsers.length === 0}>
disabled={selectedUsers.length === 0}
>
Chat
</Button>
</div>
+307 -307
View File
@@ -1,309 +1,309 @@
{
"conversations": [
{
"id": "conv1",
"profile": "https://randomuser.me/api/portraits/men/32.jpg",
"username": "alex_dev",
"fullName": "Alex John",
"title": "Senior Backend Dev",
"messages": [
{
"sender": "You",
"message": "See you later, Alex!",
"timestamp": "2024-08-24T11:15:15"
},
{
"sender": "Alex",
"message": "Alright, talk to you later!",
"timestamp": "2024-08-24T11:11:30"
},
{
"sender": "You",
"message": "For sure. Anyway, I should get back to reviewing the project.",
"timestamp": "2024-08-23T09:26:50"
},
{
"sender": "Alex",
"message": "Yeah, let me know what you think.",
"timestamp": "2024-08-23T09:25:15"
},
{
"sender": "You",
"message": "Oh, nice! I've been waiting for that. I'll check it out later.",
"timestamp": "2024-08-23T09:24:30"
},
{
"sender": "Alex",
"message": "They've added a dark mode option! It looks really sleek.",
"timestamp": "2024-08-23T09:23:10"
},
{
"sender": "You",
"message": "No, not yet. What's new?",
"timestamp": "2024-08-23T09:22:00"
},
{
"sender": "Alex",
"message": "By the way, have you seen the new feature update?",
"timestamp": "2024-08-23T09:21:05"
},
{
"sender": "You",
"message": "Will do! Thanks, Alex.",
"timestamp": "2024-08-23T09:20:10"
},
{
"sender": "Alex",
"message": "Great! Let me know if you need any help.",
"timestamp": "2024-08-23T09:19:20"
},
{
"sender": "You",
"message": "Almost done. Just need to review a few things.",
"timestamp": "2024-08-23T09:18:45"
},
{
"sender": "Alex",
"message": "I'm good, thanks! Did you finish the project?",
"timestamp": "2024-08-23T09:17:10"
},
{
"sender": "You",
"message": "Hey Alex, I'm doing well! How about you?",
"timestamp": "2024-08-23T09:16:30"
},
{
"sender": "Alex",
"message": "Hey Bob, how are you doing?",
"timestamp": "2024-08-23T09:15:00"
}
]
},
{
"id": "conv2",
"profile": "https://randomuser.me/api/portraits/women/45.jpg",
"username": "taylor.codes",
"fullName": "Taylor Grande",
"title": "Tech Lead",
"messages": [
{
"sender": "Taylor",
"message": "Yeah, it's really well-explained. You should give it a try.",
"timestamp": "2024-08-23T10:35:00"
},
{
"sender": "You",
"message": "Not yet, is it good?",
"timestamp": "2024-08-23T10:32:00"
},
{
"sender": "Taylor",
"message": "Hey, did you check out that new tutorial?",
"timestamp": "2024-08-23T10:30:00"
}
]
},
{
"id": "conv3",
"profile": "https://randomuser.me/api/portraits/men/54.jpg",
"username": "john_stack",
"fullName": "John Doe",
"title": "QA",
"messages": [
{
"sender": "You",
"message": "Yep, see ya. 👋🏼",
"timestamp": "2024-08-22T18:59:00"
},
{
"sender": "John",
"message": "Great, see you then!",
"timestamp": "2024-08-22T18:55:00"
},
{
"sender": "You",
"message": "Yes, same time as usual. I'll send the invite shortly.",
"timestamp": "2024-08-22T18:50:00"
},
{
"sender": "John",
"message": "Are we still on for the meeting tomorrow?",
"timestamp": "2024-08-22T18:45:00"
}
]
},
{
"id": "conv4",
"profile": "https://randomuser.me/api/portraits/women/29.jpg",
"username": "megan_frontend",
"fullName": "Megan Flux",
"title": "Jr Developer",
"messages": [
{
"sender": "You",
"message": "Sure ✌🏼",
"timestamp": "2024-08-23T11:30:00"
},
{
"sender": "Megan",
"message": "Thanks, appreciate it!",
"timestamp": "2024-08-23T11:30:00"
},
{
"sender": "You",
"message": "Sure thing! I'll take a look in the next hour.",
"timestamp": "2024-08-23T11:25:00"
},
{
"sender": "Megan",
"message": "Hey! Do you have time to review my PR today?",
"timestamp": "2024-08-23T11:20:00"
}
]
},
{
"id": "conv5",
"profile": "https://randomuser.me/api/portraits/men/72.jpg",
"username": "dev_david",
"fullName": "David Brown",
"title": "Senior UI/UX Designer",
"messages": [
{
"sender": "You",
"message": "Great, I'll review them now!",
"timestamp": "2024-08-23T12:00:00"
},
{
"sender": "David",
"message": "Just sent you the files. Let me know if you need any changes.",
"timestamp": "2024-08-23T11:58:00"
},
{
"sender": "David",
"message": "I finished the design for the dashboard. Thoughts?",
"timestamp": "2024-08-23T11:55:00"
}
]
},
{
"id": "conv6",
"profile": "https://randomuser.me/api/portraits/women/68.jpg",
"username": "julia.design",
"fullName": "Julia Carter",
"title": "Product Designer",
"messages": [
{
"sender": "Julia",
"message": "Same here! It's coming together nicely.",
"timestamp": "2024-08-22T14:10:00"
},
{
"sender": "You",
"message": "I'm really excited to see the final product!",
"timestamp": "2024-08-22T14:15:00"
},
{
"sender": "You",
"message": "How's the project looking on your end?",
"timestamp": "2024-08-22T14:05:00"
}
]
},
{
"id": "conv7",
"profile": "https://randomuser.me/api/portraits/men/24.jpg",
"username": "brad_dev",
"fullName": "Brad Wilson",
"title": "CEO",
"messages": [
{
"sender": "Brad",
"message": "Got it! Thanks for the update.",
"timestamp": "2024-08-23T15:45:00"
},
{
"sender": "You",
"message": "The release has been delayed to next week.",
"timestamp": "2024-08-23T15:40:00"
},
{
"sender": "Brad",
"message": "Hey, any news on the release?",
"timestamp": "2024-08-23T15:35:00"
}
]
},
{
"id": "conv8",
"profile": "https://randomuser.me/api/portraits/women/34.jpg",
"username": "katie_ui",
"fullName": "Katie Lee",
"title": "QA",
"messages": [
{
"sender": "Katie",
"message": "I'll join the call in a few minutes.",
"timestamp": "2024-08-23T09:50:00"
},
{
"sender": "You",
"message": "Perfect! We'll start as soon as you're in.",
"timestamp": "2024-08-23T09:48:00"
},
{
"sender": "Katie",
"message": "Is the meeting still on?",
"timestamp": "2024-08-23T09:45:00"
}
]
},
{
"id": "conv9",
"profile": "https://randomuser.me/api/portraits/men/67.jpg",
"username": "matt_fullstack",
"fullName": "Matt Green",
"title": "Full-stack Dev",
"messages": [
{
"sender": "Matt",
"message": "Sure thing, I'll send over the updates shortly.",
"timestamp": "2024-08-23T10:25:00"
},
{
"sender": "You",
"message": "Could you update the backend as well?",
"timestamp": "2024-08-23T10:23:00"
},
{
"sender": "Matt",
"message": "The frontend updates are done. How does it look?",
"timestamp": "2024-08-23T10:20:00"
}
]
},
{
"id": "conv10",
"profile": "https://randomuser.me/api/portraits/women/56.jpg",
"username": "sophie_dev",
"fullName": "Sophie Alex",
"title": "Jr. Frontend Dev",
"messages": [
{
"sender": "You",
"message": "Thanks! I'll review your code and get back to you.",
"timestamp": "2024-08-23T16:10:00"
},
{
"sender": "Sophie",
"message": "Let me know if you need anything else.",
"timestamp": "2024-08-23T16:05:00"
},
{
"sender": "Sophie",
"message": "The feature is implemented. Can you review it?",
"timestamp": "2024-08-23T16:00:00"
}
]
}
]
"conversations": [
{
"id": "conv1",
"profile": "https://randomuser.me/api/portraits/men/32.jpg",
"username": "alex_dev",
"fullName": "Alex John",
"title": "Senior Backend Dev",
"messages": [
{
"sender": "You",
"message": "See you later, Alex!",
"timestamp": "2024-08-24T11:15:15"
},
{
"sender": "Alex",
"message": "Alright, talk to you later!",
"timestamp": "2024-08-24T11:11:30"
},
{
"sender": "You",
"message": "For sure. Anyway, I should get back to reviewing the project.",
"timestamp": "2024-08-23T09:26:50"
},
{
"sender": "Alex",
"message": "Yeah, let me know what you think.",
"timestamp": "2024-08-23T09:25:15"
},
{
"sender": "You",
"message": "Oh, nice! I've been waiting for that. I'll check it out later.",
"timestamp": "2024-08-23T09:24:30"
},
{
"sender": "Alex",
"message": "They've added a dark mode option! It looks really sleek.",
"timestamp": "2024-08-23T09:23:10"
},
{
"sender": "You",
"message": "No, not yet. What's new?",
"timestamp": "2024-08-23T09:22:00"
},
{
"sender": "Alex",
"message": "By the way, have you seen the new feature update?",
"timestamp": "2024-08-23T09:21:05"
},
{
"sender": "You",
"message": "Will do! Thanks, Alex.",
"timestamp": "2024-08-23T09:20:10"
},
{
"sender": "Alex",
"message": "Great! Let me know if you need any help.",
"timestamp": "2024-08-23T09:19:20"
},
{
"sender": "You",
"message": "Almost done. Just need to review a few things.",
"timestamp": "2024-08-23T09:18:45"
},
{
"sender": "Alex",
"message": "I'm good, thanks! Did you finish the project?",
"timestamp": "2024-08-23T09:17:10"
},
{
"sender": "You",
"message": "Hey Alex, I'm doing well! How about you?",
"timestamp": "2024-08-23T09:16:30"
},
{
"sender": "Alex",
"message": "Hey Bob, how are you doing?",
"timestamp": "2024-08-23T09:15:00"
}
]
},
{
"id": "conv2",
"profile": "https://randomuser.me/api/portraits/women/45.jpg",
"username": "taylor.codes",
"fullName": "Taylor Grande",
"title": "Tech Lead",
"messages": [
{
"sender": "Taylor",
"message": "Yeah, it's really well-explained. You should give it a try.",
"timestamp": "2024-08-23T10:35:00"
},
{
"sender": "You",
"message": "Not yet, is it good?",
"timestamp": "2024-08-23T10:32:00"
},
{
"sender": "Taylor",
"message": "Hey, did you check out that new tutorial?",
"timestamp": "2024-08-23T10:30:00"
}
]
},
{
"id": "conv3",
"profile": "https://randomuser.me/api/portraits/men/54.jpg",
"username": "john_stack",
"fullName": "John Doe",
"title": "QA",
"messages": [
{
"sender": "You",
"message": "Yep, see ya. 👋🏼",
"timestamp": "2024-08-22T18:59:00"
},
{
"sender": "John",
"message": "Great, see you then!",
"timestamp": "2024-08-22T18:55:00"
},
{
"sender": "You",
"message": "Yes, same time as usual. I'll send the invite shortly.",
"timestamp": "2024-08-22T18:50:00"
},
{
"sender": "John",
"message": "Are we still on for the meeting tomorrow?",
"timestamp": "2024-08-22T18:45:00"
}
]
},
{
"id": "conv4",
"profile": "https://randomuser.me/api/portraits/women/29.jpg",
"username": "megan_frontend",
"fullName": "Megan Flux",
"title": "Jr Developer",
"messages": [
{
"sender": "You",
"message": "Sure ✌🏼",
"timestamp": "2024-08-23T11:30:00"
},
{
"sender": "Megan",
"message": "Thanks, appreciate it!",
"timestamp": "2024-08-23T11:30:00"
},
{
"sender": "You",
"message": "Sure thing! I'll take a look in the next hour.",
"timestamp": "2024-08-23T11:25:00"
},
{
"sender": "Megan",
"message": "Hey! Do you have time to review my PR today?",
"timestamp": "2024-08-23T11:20:00"
}
]
},
{
"id": "conv5",
"profile": "https://randomuser.me/api/portraits/men/72.jpg",
"username": "dev_david",
"fullName": "David Brown",
"title": "Senior UI/UX Designer",
"messages": [
{
"sender": "You",
"message": "Great, I'll review them now!",
"timestamp": "2024-08-23T12:00:00"
},
{
"sender": "David",
"message": "Just sent you the files. Let me know if you need any changes.",
"timestamp": "2024-08-23T11:58:00"
},
{
"sender": "David",
"message": "I finished the design for the dashboard. Thoughts?",
"timestamp": "2024-08-23T11:55:00"
}
]
},
{
"id": "conv6",
"profile": "https://randomuser.me/api/portraits/women/68.jpg",
"username": "julia.design",
"fullName": "Julia Carter",
"title": "Product Designer",
"messages": [
{
"sender": "Julia",
"message": "Same here! It's coming together nicely.",
"timestamp": "2024-08-22T14:10:00"
},
{
"sender": "You",
"message": "I'm really excited to see the final product!",
"timestamp": "2024-08-22T14:15:00"
},
{
"sender": "You",
"message": "How's the project looking on your end?",
"timestamp": "2024-08-22T14:05:00"
}
]
},
{
"id": "conv7",
"profile": "https://randomuser.me/api/portraits/men/24.jpg",
"username": "brad_dev",
"fullName": "Brad Wilson",
"title": "CEO",
"messages": [
{
"sender": "Brad",
"message": "Got it! Thanks for the update.",
"timestamp": "2024-08-23T15:45:00"
},
{
"sender": "You",
"message": "The release has been delayed to next week.",
"timestamp": "2024-08-23T15:40:00"
},
{
"sender": "Brad",
"message": "Hey, any news on the release?",
"timestamp": "2024-08-23T15:35:00"
}
]
},
{
"id": "conv8",
"profile": "https://randomuser.me/api/portraits/women/34.jpg",
"username": "katie_ui",
"fullName": "Katie Lee",
"title": "QA",
"messages": [
{
"sender": "Katie",
"message": "I'll join the call in a few minutes.",
"timestamp": "2024-08-23T09:50:00"
},
{
"sender": "You",
"message": "Perfect! We'll start as soon as you're in.",
"timestamp": "2024-08-23T09:48:00"
},
{
"sender": "Katie",
"message": "Is the meeting still on?",
"timestamp": "2024-08-23T09:45:00"
}
]
},
{
"id": "conv9",
"profile": "https://randomuser.me/api/portraits/men/67.jpg",
"username": "matt_fullstack",
"fullName": "Matt Green",
"title": "Full-stack Dev",
"messages": [
{
"sender": "Matt",
"message": "Sure thing, I'll send over the updates shortly.",
"timestamp": "2024-08-23T10:25:00"
},
{
"sender": "You",
"message": "Could you update the backend as well?",
"timestamp": "2024-08-23T10:23:00"
},
{
"sender": "Matt",
"message": "The frontend updates are done. How does it look?",
"timestamp": "2024-08-23T10:20:00"
}
]
},
{
"id": "conv10",
"profile": "https://randomuser.me/api/portraits/women/56.jpg",
"username": "sophie_dev",
"fullName": "Sophie Alex",
"title": "Jr. Frontend Dev",
"messages": [
{
"sender": "You",
"message": "Thanks! I'll review your code and get back to you.",
"timestamp": "2024-08-23T16:10:00"
},
{
"sender": "Sophie",
"message": "Let me know if you need anything else.",
"timestamp": "2024-08-23T16:05:00"
},
{
"sender": "Sophie",
"message": "The feature is implemented. Can you review it?",
"timestamp": "2024-08-23T16:00:00"
}
]
}
]
}
+39 -24
View File
@@ -12,7 +12,7 @@ import {
IconPlus,
IconSearch,
IconSend,
IconVideo
IconVideo,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -33,14 +33,14 @@ export default function Chats() {
const [search, setSearch] = useState("");
const [selectedUser, setSelectedUser] = useState<ChatUser | null>(null);
const [mobileSelectedUser, setMobileSelectedUser] = useState<ChatUser | null>(
null
null,
);
const [createConversationDialogOpened, setCreateConversationDialog] =
useState(false);
// Filtered data based on the search query
const filteredChatList = conversations.filter(({ fullName }) =>
fullName.toLowerCase().includes(search.trim().toLowerCase())
fullName.toLowerCase().includes(search.trim().toLowerCase()),
);
const currentMessage = selectedUser?.messages.reduce(
@@ -57,7 +57,7 @@ export default function Chats() {
return acc;
},
{}
{},
);
const users = conversations.map(({ messages, ...user }) => user);
@@ -88,7 +88,8 @@ export default function Chats() {
size="icon"
variant="ghost"
onClick={() => setCreateConversationDialog(true)}
className="rounded-lg">
className="rounded-lg"
>
<IconEdit size={24} className="stroke-muted-foreground" />
</Button>
</div>
@@ -120,12 +121,13 @@ export default function Chats() {
type="button"
className={cn(
`-mx-1 flex w-full rounded-md px-2 py-2 text-left text-sm hover:bg-secondary/75`,
selectedUser?.id === id && "sm:bg-muted"
selectedUser?.id === id && "sm:bg-muted",
)}
onClick={() => {
setSelectedUser(chatUsr);
setMobileSelectedUser(chatUsr);
}}>
}}
>
<div className="flex gap-2">
<Avatar>
<AvatarImage src={profile} alt={username} />
@@ -153,8 +155,9 @@ export default function Chats() {
<div
className={cn(
"absolute inset-0 left-full z-50 hidden w-full flex-1 flex-col rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex",
mobileSelectedUser && "left-0 flex"
)}>
mobileSelectedUser && "left-0 flex",
)}
>
{/* Top Part */}
<div className="mb-1 flex flex-none justify-between rounded-t-md bg-secondary p-4 shadow-lg">
{/* Left */}
@@ -163,7 +166,8 @@ export default function Chats() {
size="icon"
variant="ghost"
className="-ml-2 h-full sm:hidden"
onClick={() => setMobileSelectedUser(null)}>
onClick={() => setMobileSelectedUser(null)}
>
<IconArrowLeft />
</Button>
<div className="flex items-center gap-2 lg:gap-4">
@@ -190,19 +194,22 @@ export default function Chats() {
<Button
size="icon"
variant="ghost"
className="hidden size-8 rounded-full sm:inline-flex lg:size-10">
className="hidden size-8 rounded-full sm:inline-flex lg:size-10"
>
<IconVideo size={22} className="stroke-muted-foreground" />
</Button>
<Button
size="icon"
variant="ghost"
className="hidden size-8 rounded-full sm:inline-flex lg:size-10">
className="hidden size-8 rounded-full sm:inline-flex lg:size-10"
>
<IconPhone size={22} className="stroke-muted-foreground" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-10 rounded-md sm:h-8 sm:w-4 lg:h-10 lg:w-6">
className="h-10 rounded-md sm:h-8 sm:w-4 lg:h-10 lg:w-6"
>
<IconDotsVertical className="stroke-muted-foreground sm:size-5" />
</Button>
</div>
@@ -223,14 +230,16 @@ export default function Chats() {
"chat-box max-w-72 break-words px-3 py-2 shadow-lg",
msg.sender === "You"
? "self-end rounded-[16px_16px_0_16px] bg-primary/85 text-primary-foreground/75"
: "self-start rounded-[16px_16px_16px_0] bg-secondary"
)}>
: "self-start rounded-[16px_16px_16px_0] bg-secondary",
)}
>
{msg.message}{" "}
<span
className={cn(
"mt-1 block text-xs font-light italic text-muted-foreground",
msg.sender === "You" && "text-right"
)}>
msg.sender === "You" && "text-right",
)}
>
{format(msg.timestamp, "h:mm a")}
</span>
</div>
@@ -248,7 +257,8 @@ export default function Chats() {
size="icon"
type="button"
variant="ghost"
className="h-8 rounded-md">
className="h-8 rounded-md"
>
<IconPlus
size={20}
className="stroke-muted-foreground"
@@ -258,7 +268,8 @@ export default function Chats() {
size="icon"
type="button"
variant="ghost"
className="hidden h-8 rounded-md lg:inline-flex">
className="hidden h-8 rounded-md lg:inline-flex"
>
<IconPhotoPlus
size={20}
className="stroke-muted-foreground"
@@ -268,7 +279,8 @@ export default function Chats() {
size="icon"
type="button"
variant="ghost"
className="hidden h-8 rounded-md lg:inline-flex">
className="hidden h-8 rounded-md lg:inline-flex"
>
<IconPaperclip
size={20}
className="stroke-muted-foreground"
@@ -286,7 +298,8 @@ export default function Chats() {
<Button
variant="ghost"
size="icon"
className="hidden sm:inline-flex">
className="hidden sm:inline-flex"
>
<IconSend size={20} />
</Button>
</div>
@@ -299,8 +312,9 @@ export default function Chats() {
) : (
<div
className={cn(
"absolute inset-0 left-full z-50 hidden w-full flex-1 flex-col justify-center rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex"
)}>
"absolute inset-0 left-full z-50 hidden w-full flex-1 flex-col justify-center rounded-md border bg-primary-foreground shadow-sm transition-all duration-200 sm:static sm:z-auto sm:flex",
)}
>
<div className="flex flex-col items-center space-y-6">
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-white">
<IconMessages className="h-8 w-8" />
@@ -313,7 +327,8 @@ export default function Chats() {
</div>
<Button
className="bg-blue-500 px-6 text-white hover:bg-blue-600"
onClick={() => setCreateConversationDialog(true)}>
onClick={() => setCreateConversationDialog(true)}
>
Send message
</Button>
</div>
@@ -3,52 +3,52 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
const data = [
{
name: "Jan",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Feb",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Mar",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Apr",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "May",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jun",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Jul",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Aug",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Sep",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Oct",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Nov",
total: Math.floor(Math.random() * 5000) + 1000
total: Math.floor(Math.random() * 5000) + 1000,
},
{
name: "Dec",
total: Math.floor(Math.random() * 5000) + 1000
}
total: Math.floor(Math.random() * 5000) + 1000,
},
];
export function Overview() {
+13 -22
View File
@@ -3,9 +3,9 @@ import {
CardContent,
CardDescription,
CardHeader,
CardTitle
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { Header } from "@/components/layout/header";
import { Main } from "@/components/layout/main";
import { ProfileDropdown } from "@/components/profile-dropdown";
@@ -34,21 +34,8 @@ export default function Dashboard() {
<Tabs
orientation="vertical"
defaultValue="overview"
className="space-y-4">
<div className="w-full overflow-x-auto pb-2">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics" disabled>
Analytics
</TabsTrigger>
<TabsTrigger value="reports" disabled>
Reports
</TabsTrigger>
<TabsTrigger value="notifications" disabled>
Notifications
</TabsTrigger>
</TabsList>
</div>
className="space-y-4"
>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
@@ -64,7 +51,8 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground">
className="h-4 w-4 text-muted-foreground"
>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
</CardHeader>
@@ -88,7 +76,8 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground">
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
@@ -112,7 +101,8 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground">
className="h-4 w-4 text-muted-foreground"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<path d="M2 10h20" />
</svg>
@@ -137,7 +127,8 @@ export default function Dashboard() {
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground">
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
@@ -175,4 +166,4 @@ export default function Dashboard() {
</Main>
</>
);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ interface GeneralErrorProps extends React.HTMLAttributes<HTMLDivElement> {
export default function GeneralError({
className,
minimal = false
minimal = false,
}: GeneralErrorProps) {
const navigate = useNavigate();
const { history } = useRouter();
@@ -1,11 +1,13 @@
import { Button } from "@/components/ui/button";
export default function CallToAction() {
return (
<div className="text-center py-12">
<h2 className="text-3xl font-bold mb-4">Ready to Get Started?</h2>
<p className="mb-6 text-gray-600">Join today and start managing your shop with ease.</p>
<Button className="px-6 py-2">Sign Up Now</Button>
</div>
);
};
return (
<div className="text-center py-12">
<h2 className="text-3xl font-bold mb-4">Ready to Get Started?</h2>
<p className="mb-6 text-gray-600">
Join today and start managing your shop with ease.
</p>
<Button className="px-6 py-2">Sign Up Now</Button>
</div>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@ import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
AccordionTrigger,
} from "@/components/ui/accordion";
import { faqs } from "../data/faq-data";
@@ -5,7 +5,9 @@ import { Button } from "@/components/ui/button";
export default function Pricing() {
return (
<div className="max-w-4xl text-center">
<h2 id="pricing" className="mb-6 text-3xl font-bold">Pricing Plans</h2>
<h2 id="pricing" className="mb-6 text-3xl font-bold">
Pricing Plans
</h2>
<p className="mb-6 text-gray-600">
Prices are subject to change. These estimates help guide your decision.
</p>
+4 -4
View File
@@ -2,14 +2,14 @@ export const faqs = [
{
question: "How do I get started?",
answer:
"Sign up for a free account and start integrating our API into your shop."
"Sign up for a free account and start integrating our API into your shop.",
},
{
question: "Can I upgrade or downgrade my plan?",
answer: "Yes, you can change your plan anytime from your dashboard."
answer: "Yes, you can change your plan anytime from your dashboard.",
},
{
question: "Is there customer support?",
answer: "Yes, we offer different levels of support depending on your plan."
}
answer: "Yes, we offer different levels of support depending on your plan.",
},
];
+8 -8
View File
@@ -2,30 +2,30 @@ export const features = [
{
title: "Inventory Control",
description:
"Easily manage product stock, availability, and updates in real time."
"Easily manage product stock, availability, and updates in real time.",
},
{
title: "Flexible API",
description:
"Build custom storefronts with a backend that adapts to your needs."
"Build custom storefronts with a backend that adapts to your needs.",
},
{
title: "Seamless Transactions",
description: "Handle purchases smoothly with minimal backend complexity."
description: "Handle purchases smoothly with minimal backend complexity.",
},
{
title: "User Roles & Permissions",
description:
"Predefined roles (Owner, Manager, Employee) with different access levels to manage store operations efficiently."
"Predefined roles (Owner, Manager, Employee) with different access levels to manage store operations efficiently.",
},
{
title: "Multi-Currency Support",
description:
"Each shop can define its default currency to handle transactions in the appropriate financial context."
"Each shop can define its default currency to handle transactions in the appropriate financial context.",
},
{
title: "Future-Ready Analytics",
description:
"Upcoming analytics and statistics to help businesses track performance and make data-driven decisions."
}
];
"Upcoming analytics and statistics to help businesses track performance and make data-driven decisions.",
},
];
+31 -19
View File
@@ -1,20 +1,32 @@
export const plans = [
{
title: "Free Plan",
price: "$0/month",
description: "Basic features for small shops starting out.",
features: ["Basic Inventory Management", "Limited API Requests", "Community Support"]
},
{
title: "Pro Plan",
price: "$29/month",
description: "For growing businesses needing more control.",
features: ["Advanced Inventory Management", "Higher API Limits", "Email Support"]
},
{
title: "Enterprise Plan",
price: "$99/month",
description: "For high-scale businesses requiring full access.",
features: ["Unlimited API Access", "Priority Support", "Custom Integrations"]
}
];
{
title: "Free Plan",
price: "$0/month",
description: "Basic features for small shops starting out.",
features: [
"Basic Inventory Management",
"Limited API Requests",
"Community Support",
],
},
{
title: "Pro Plan",
price: "$29/month",
description: "For growing businesses needing more control.",
features: [
"Advanced Inventory Management",
"Higher API Limits",
"Email Support",
],
},
{
title: "Enterprise Plan",
price: "$99/month",
description: "For high-scale businesses requiring full access.",
features: [
"Unlimited API Access",
"Priority Support",
"Custom Integrations",
],
},
];
-1
View File
@@ -10,7 +10,6 @@ export default function MainPage() {
<>
<MainNavbar />
<div className="flex min-h-screen flex-col items-center justify-center overflow-hidden p-6 gap-24">
{/* Top screen padding */}
<div className="h-[20vh] min-h-[20vh]"></div>
<Hero />
@@ -6,7 +6,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -37,7 +37,7 @@ export function ProductDialog({
onImageUpload,
submitLabel,
imagePreview,
showDeleteButton = false
showDeleteButton = false,
}: ProductDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -76,7 +76,7 @@ export function ProductDialog({
id="price"
{...form.register("price", {
valueAsNumber: true,
setValueAs: (v) => parseFloat(v).toFixed(2)
setValueAs: (v) => parseFloat(v).toFixed(2),
})}
placeholder="0.00"
/>
@@ -10,7 +10,7 @@ const schema = z.object({
description: z.string().min(1, "Description is required"),
price: z.coerce.number().min(0),
stock_quantity: z.coerce.number().int().min(0),
image_data: z.string().optional()
image_data: z.string().optional(),
});
type ProductForm = z.infer<typeof schema>;
@@ -20,7 +20,7 @@ export function ProductForm({
form,
imagePreview,
onImageUpload,
submitLabel
submitLabel,
}: {
onSubmit: () => void;
form: UseFormReturn<typeof ProductForm>;
@@ -4,16 +4,16 @@ import {
Card,
CardDescription,
CardHeader,
CardTitle
CardTitle,
} from "@/components/ui/card";
export function ProductList({
currency,
products,
isLoading,
onClick
onClick,
}: {
currency: string,
currency: string;
products: ProductWithDetails[];
isLoading: boolean;
onClick: (id: number) => void;
@@ -26,7 +26,8 @@ export function ProductList({
<Card
key={product.id}
onClick={() => onClick(product.id)}
className="cursor-pointer overflow-hidden">
className="cursor-pointer overflow-hidden"
>
<div className="relative aspect-square w-full overflow-hidden">
{product.images.length > 0 ? (
<img
@@ -4,13 +4,15 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
export const schema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().min(1, "Description is required"),
price: z.coerce.number().min(0.01, "Price must be greater than 0.01").step(0.01),
price: z.coerce
.number()
.min(0.01, "Price must be greater than 0.01")
.step(0.01),
stock_quantity: z.coerce.number().int().min(0),
image_data: z.string().optional()
image_data: z.string().optional(),
});
export type ProductForm = z.infer<typeof schema>;
@@ -23,8 +25,8 @@ export function useProductForm(productData?: ProductWithDetails | null) {
description: "",
price: 0,
stock_quantity: 0,
image_data: ""
}
image_data: "",
},
});
useEffect(() => {
@@ -34,7 +36,7 @@ export function useProductForm(productData?: ProductWithDetails | null) {
description: productData.description,
price: productData.price,
stock_quantity: productData.stock_quantity,
image_data: productData.images[0]?.image_url || ""
image_data: productData.images[0]?.image_url || "",
});
}
}, [productData, form]);
+9 -7
View File
@@ -2,7 +2,7 @@ import {
IconAdjustmentsHorizontal,
IconSortAscendingLetters,
IconSortDescendingLetters,
IconPlus
IconPlus,
} from "@tabler/icons-react";
import { Header } from "@/components/layout/header";
@@ -17,7 +17,7 @@ import {
SelectTrigger,
SelectValue,
SelectContent,
SelectItem
SelectItem,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
@@ -60,7 +60,7 @@ export default function Products() {
description: "",
price: 0,
stock_quantity: 0,
image_data: ""
image_data: "",
});
setSelectedProductId(undefined);
setDialogOpen(true);
@@ -89,11 +89,11 @@ export default function Products() {
const sortedProducts = [...products].sort((a, b) =>
sort === "ascending"
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name)
: b.name.localeCompare(a.name),
);
const filteredProducts = sortedProducts.filter((product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase())
product.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
return (
@@ -125,7 +125,8 @@ export default function Products() {
/>
<Select
value={sort}
onValueChange={(v) => setSort(v as "ascending" | "descending")}>
onValueChange={(v) => setSort(v as "ascending" | "descending")}
>
<SelectTrigger className="w-16">
<SelectValue>
<IconAdjustmentsHorizontal size={18} />
@@ -163,7 +164,8 @@ export default function Products() {
<Button
onClick={() => handleCreateClicked()}
className="absolute bottom-0 z-10 mb-4 size-14 rounded-full p-0 shadow-lg hover:brightness-110"
variant="default">
variant="default"
>
<IconPlus size={28} />
</Button>
</Main>
@@ -0,0 +1,198 @@
"use client";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "@/hooks/useToast";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Coupon } from "../data/schema";
import { useCoupon } from "@/hooks/useCoupon";
const formSchema = z.object({
name: z.string().min(1, { message: "Name is required." }),
text: z.string().min(1, { message: "Text is required." }),
valid_due: z.coerce.date({
errorMap: () => ({ message: "Invalid date." }),
}),
discount_amount: z
.number({ invalid_type_error: "Must be a number." })
.min(1, { message: "Discount must be at least 1%." })
.max(100, { message: "Discount can't exceed 100%." }),
isEdit: z.boolean(),
});
type CouponForm = z.infer<typeof formSchema>;
interface Props {
currentRow?: Coupon;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function CouponsActionDialog({ currentRow, open, onOpenChange }: Props) {
const { createCoupon, updateCoupon } = useCoupon(currentRow?.id);
const isEdit = !!currentRow;
const form = useForm<CouponForm>({
resolver: zodResolver(formSchema),
defaultValues: isEdit
? {
...currentRow,
isEdit,
}
: {
name: "",
text: "",
valid_due: new Date(),
discount_amount: 10,
isEdit,
},
});
const onSubmit = (values: CouponForm) => {
try {
const payload = {
...values,
valid_due: values.valid_due.toISOString(), // convert to string
};
if (isEdit) {
updateCoupon.mutate({ ...currentRow, ...payload });
} else {
createCoupon.mutate({
...payload,
});
}
toast({
title: isEdit ? "Coupon updated" : "Coupon created",
description: `${values.name} (${values.discount_amount}%)`,
});
form.reset();
onOpenChange(false);
} catch (err) {
toast({
title: "An error occurred",
description: (err as Error).message,
variant: "destructive",
});
}
};
return (
<Dialog
open={open}
onOpenChange={(state) => {
form.reset();
onOpenChange(state);
}}
>
<DialogContent className="sm:max-w-lg">
<DialogHeader className="text-left">
<DialogTitle>{isEdit ? "Edit Coupon" : "Add New Coupon"}</DialogTitle>
<DialogDescription>
{isEdit ? "Update the coupon below." : "Create a new coupon."}
</DialogDescription>
</DialogHeader>
<ScrollArea className="-mr-4 h-[26.25rem] w-full py-1 pr-4">
<Form {...form}>
<form
id="coupon-form"
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4 p-0.5"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Spring Sale" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="text"
render={({ field }) => (
<FormItem>
<FormLabel>Text</FormLabel>
<FormControl>
<Input placeholder="Get 20% off all items!" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="discount_amount"
render={({ field }) => (
<FormItem>
<FormLabel>Discount (%)</FormLabel>
<FormControl>
<Input
type="number"
placeholder="e.g., 10"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="valid_due"
render={({ field }) => (
<FormItem>
<FormLabel>Valid Until</FormLabel>
<FormControl>
<Input
type="datetime-local"
value={field.value.toISOString().slice(0, 16)}
onChange={(e) =>
field.onChange(new Date(e.target.value))
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<DialogFooter>
<Button type="submit" form="coupon-form">
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,121 @@
import { ColumnDef } from "@tanstack/react-table";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { Coupon } from "../data/schema";
import { DataTableColumnHeader } from "./data-table-column-header";
import { DataTableRowActions } from "./data-table-row-actions";
import { Badge } from "@/components/ui/badge";
import { format, isBefore } from "date-fns";
import { couponStatusColors, couponTypes } from "../data/data";
export const columns: ColumnDef<Coupon>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-[2px]"
/>
),
meta: {
className: cn(
"sticky md:table-cell left-0 z-10 rounded-tl",
"bg-background transition-colors duration-200 group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
),
},
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-[2px]"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => (
<div className="font-medium">{row.getValue("name")}</div>
),
enableHiding: false,
},
{
accessorKey: "text",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Text" />
),
cell: ({ row }) => (
<div className="max-w-64 truncate text-muted-foreground">
{row.getValue("text")}
</div>
),
},
{
accessorKey: "Valid Due",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Valid Until" />
),
cell: ({ row }) => {
const { valid_due } = row.original;
return <div>{format(new Date(valid_due), "PPPp")}</div>;
},
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const validDue = new Date(row.original.valid_due);
const now = new Date();
const isExpired = isBefore(validDue, now);
const status = isExpired ? "expired" : "active";
const badgeColor = couponStatusColors.get(status);
const statusMeta = couponTypes.find((t) => t.value === status);
return (
<div className="flex items-center gap-x-2">
{statusMeta?.icon && (
<statusMeta.icon size={16} className="text-muted-foreground" />
)}
<Badge variant="outline" className={cn("capitalize", badgeColor)}>
{statusMeta?.label}
</Badge>
</div>
);
},
filterFn: (row, id, value) => {
const validDue = new Date(row.original.valid_due);
const now = new Date();
const status = isBefore(validDue, now) ? "expired" : "active";
return value.includes(status);
},
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "Discount",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Discount" />
),
cell: ({ row }) => {
const { discount_amount } = row.original;
return <Badge variant="outline">{discount_amount}%</Badge>;
},
},
{
id: "actions",
cell: DataTableRowActions,
},
];
@@ -0,0 +1,65 @@
"use client";
import { toast } from "@/hooks/useToast";
import { IconAlertTriangle } from "@tabler/icons-react";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { Coupon } from "../data/schema";
import { useCoupon } from "@/hooks/useCoupon";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
currentRow: Coupon;
}
export function CouponsDeleteDialog({ open, onOpenChange, currentRow }: Props) {
const { deleteCoupon } = useCoupon(currentRow.id);
const handleDelete = () => {
deleteCoupon.mutate(currentRow.id);
onOpenChange(false);
toast({
title: "Coupon deleted",
description: `"${currentRow.name}" (${currentRow.discount_amount}% off) has been removed.`,
});
};
return (
<ConfirmDialog
open={open}
onOpenChange={onOpenChange}
handleConfirm={handleDelete}
title={
<span className="text-destructive">
<IconAlertTriangle
className="mr-1 inline-block stroke-destructive"
size={18}
/>{" "}
Delete Coupon
</span>
}
desc={
<div className="space-y-4">
<p>
Are you sure you want to delete the coupon{" "}
<span className="font-bold">{currentRow.name}</span>?
<br />
This action cannot be undone.
</p>
<Alert variant="destructive">
<AlertTitle>Warning!</AlertTitle>
<AlertDescription>
Once removed, this coupon will no longer be available to
customers.
</AlertDescription>
</Alert>
</div>
}
confirmText="Delete"
destructive
/>
);
}
@@ -0,0 +1,45 @@
import { useCouponsContext } from "../context/coupon-context";
import { CouponsActionDialog } from "./coupon-action-dialog";
import { CouponsDeleteDialog } from "./coupon-delete-dialog";
export function CouponsDialogs() {
const { open, setOpen, currentRow, setCurrentRow } = useCouponsContext();
return (
<>
<CouponsActionDialog
key="coupon-add"
open={open === "add"}
onOpenChange={() => setOpen("add")}
/>
{currentRow && (
<>
<CouponsActionDialog
key={`coupon-edit-${currentRow.id}`}
open={open === "edit"}
onOpenChange={() => {
setOpen("edit");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
currentRow={currentRow}
/>
<CouponsDeleteDialog
key={`coupon-delete-${currentRow.id}`}
open={open === "delete"}
onOpenChange={() => {
setOpen("delete");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
currentRow={currentRow}
/>
</>
)}
</>
);
}
@@ -0,0 +1,14 @@
import { IconPlus } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { useCouponsContext } from "../context/coupon-context";
export function CouponPrimaryButtons() {
const { setOpen } = useCouponsContext();
return (
<div className="flex gap-2">
<Button className="space-x-1" onClick={() => setOpen("add")}>
<span>Add Coupon</span> <IconPlus size={18} />
</Button>
</div>
);
}
@@ -0,0 +1,131 @@
import { useState } from "react";
import {
ColumnDef,
ColumnFiltersState,
RowData,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Coupon } from "../data/schema";
import { DataTablePagination } from "./data-table-pagination";
import { DataTableToolbar } from "./data-table-toolbar";
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
className: string;
}
}
interface DataTableProps {
columns: ColumnDef<Coupon>[];
data: Coupon[];
}
export function CouponsTable({ columns, data }: DataTableProps) {
const [rowSelection, setRowSelection] = useState({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
<div className="space-y-4">
<DataTableToolbar table={table} />
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="group/row">
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
colSpan={header.colSpan}
className={header.column.columnDef.meta?.className ?? ""}
>
{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"}
className="group/row"
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.className ?? ""}
>
{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>
<DataTablePagination table={table} />
</div>
);
}
@@ -0,0 +1,74 @@
import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title: string;
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
}
return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
{column.getCanHide() && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
@@ -0,0 +1,145 @@
import * as React from "react";
import { CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
options: {
label: string;
value: string;
icon?: React.ComponentType<{ className?: string }>;
}[];
}
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircledIcon className="h-4 w-4" />
{title}
{selectedValues?.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value);
} else {
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(
filterValues.length ? filterValues : undefined,
);
}}
>
<div
className={cn(
"flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
);
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,99 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
}
export function DataTablePagination<TData>({
table,
}: DataTablePaginationProps<TData>) {
return (
<div
className="flex items-center justify-between overflow-clip px-2"
style={{ overflowClipMargin: 1 }}
>
<div className="hidden flex-1 text-sm text-muted-foreground sm:block">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center sm:space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="hidden text-sm font-medium sm:block">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,61 @@
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Row } from "@tanstack/react-table";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useCouponsContext } from "../context/coupon-context";
import { Coupon } from "../data/schema";
interface DataTableRowActionsProps {
row: Row<Coupon>;
}
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { setOpen, setCurrentRow } = useCouponsContext();
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem
onClick={() => {
setCurrentRow(row.original);
setOpen("edit");
}}
>
Edit
<DropdownMenuShortcut>
<IconEdit size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
setCurrentRow(row.original);
setOpen("delete");
}}
className="!text-red-500"
>
Delete
<DropdownMenuShortcut>
<IconTrash size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,52 @@
import { Cross2Icon } from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { couponTypes } from "../data/data";
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
import { DataTableViewOptions } from "./data-table-view-options";
interface DataTableToolbarProps<TData> {
table: Table<TData>;
}
export function DataTableToolbar<TData>({
table,
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
return (
<div className="flex items-center justify-between">
<div className="flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2">
<Input
placeholder="Filter coupons..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
<div className="flex gap-x-2">
{table.getColumn("valid_due") && (
<DataTableFacetedFilter
column={table.getColumn("valid_due")}
title="Status"
options={couponTypes.map((t) => ({ ...t }))}
/>
)}
</div>
{isFiltered && (
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3"
>
Reset
<Cross2Icon className="ml-2 h-4 w-4" />
</Button>
)}
</div>
<DataTableViewOptions table={table} />
</div>
);
}
@@ -0,0 +1,56 @@
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { MixerHorizontalIcon } from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface DataTableViewOptionsProps<TData> {
table: Table<TData>;
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide(),
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -0,0 +1,42 @@
import React, { useState } from "react";
import useDialogState from "@/hooks/useDialogState";
import { Coupon } from "../data/schema";
type CouponsDialogType = "add" | "edit" | "delete";
interface CouponsContextType {
open: CouponsDialogType | null;
setOpen: (type: CouponsDialogType | null) => void;
currentRow: Coupon | null;
setCurrentRow: React.Dispatch<React.SetStateAction<Coupon | null>>;
}
const CouponsContext = React.createContext<CouponsContextType | null>(null);
interface Props {
children: React.ReactNode;
}
export default function CouponsProvider({ children }: Props) {
const [open, setOpen] = useDialogState<CouponsDialogType>(null);
const [currentRow, setCurrentRow] = useState<Coupon | null>(null);
return (
<CouponsContext.Provider
value={{ open, setOpen, currentRow, setCurrentRow }}
>
{children}
</CouponsContext.Provider>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useCouponsContext = () => {
const context = React.useContext(CouponsContext);
if (!context) {
throw new Error("useCouponsContext must be used within <CouponsProvider>");
}
return context;
};
@@ -0,0 +1,25 @@
import { IconClockCancel, IconClock } from "@tabler/icons-react";
export const couponStatusColors = new Map<"active" | "expired", string>([
[
"active",
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100 border border-green-300 dark:border-green-600",
],
[
"expired",
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100 border border-red-300 dark:border-red-600",
],
]);
export const couponTypes = [
{
label: "Active",
value: "active",
icon: IconClock,
},
{
label: "Expired",
value: "expired",
icon: IconClockCancel,
},
] as const;
@@ -0,0 +1,14 @@
import { z } from "zod";
// Coupon schema
export const couponSchema = z.object({
id: z.number(),
name: z.string().max(200),
text: z.string().max(50),
valid_due: z.coerce.date(),
discount_amount: z.number().min(1).max(100),
});
export type Coupon = z.infer<typeof couponSchema>;
export const couponListSchema = z.array(couponSchema);
@@ -0,0 +1,69 @@
import { Header } from "@/components/layout/header";
import { Main } from "@/components/layout/main";
import { ProfileDropdown } from "@/components/profile-dropdown";
import { Search } from "@/components/search";
import { ThemeSwitch } from "@/components/theme-switch";
import { columns } from "./components/coupon-columns";
import { CouponsDialogs } from "./components/coupon-dialog";
import { CouponPrimaryButtons } from "./components/coupon-primary-buttons";
import { CouponsTable } from "./components/coupon-table";
import CouponsProvider from "./context/coupon-context";
import { Coupon, couponListSchema } from "./data/schema";
import { useCoupons } from "@/hooks/useCoupons";
export default function Coupons() {
const { data, error, isLoading } = useCoupons();
let couponList: Coupon[] = [];
if (data) {
try {
couponList = couponListSchema.parse(data);
} catch (e) {
console.error("Schema validation failed:", e);
couponList = [];
}
}
console.log(couponList);
return (
<CouponsProvider>
<Header fixed>
<Search />
<div className="ml-auto flex items-center space-x-4">
<ThemeSwitch />
<ProfileDropdown />
</div>
</Header>
<Main>
<div className="mb-2 flex flex-wrap items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">Coupons</h2>
<p className="text-muted-foreground">
Create, edit and manage product discounts.
</p>
</div>
<CouponPrimaryButtons />
</div>
{isLoading && (
<p className="text-muted-foreground">Loading coupons...</p>
)}
{error && (
<p className="text-red-500">
Failed to load coupons: {(error as Error).message}
</p>
)}
{!isLoading && !error && (
<div className="-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0">
<CouponsTable data={couponList} columns={columns} />
</div>
)}
</Main>
<CouponsDialogs />
</CouponsProvider>
);
}
@@ -2,14 +2,14 @@ import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import
{
Form,
FormControl, FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import useAuth from "@/hooks/useAuth";
@@ -46,7 +46,7 @@ export function AccountForm() {
const { updateAccountMutation, user } = useAuth();
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema)
resolver: zodResolver(accountFormSchema),
});
function onSubmit(data: AccountFormValues) {
@@ -57,7 +57,7 @@ export function AccountForm() {
phone_number: data.phone_number,
username: data.username,
first_name: data.first_name,
last_name: data.last_name
last_name: data.last_name,
});
}
@@ -71,7 +71,11 @@ export function AccountForm() {
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Your username" defaultValue={user?.username} {...field} />
<Input
placeholder="Your username"
defaultValue={user?.username}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -84,7 +88,11 @@ export function AccountForm() {
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Your email" defaultValue={user?.email} {...field} />
<Input
placeholder="Your email"
defaultValue={user?.email}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -97,7 +105,11 @@ export function AccountForm() {
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input placeholder="Your phone number" defaultValue={user?.phone_number} {...field} />
<Input
placeholder="Your phone number"
defaultValue={user?.phone_number}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -120,4 +132,4 @@ export function AccountForm() {
</form>
</Form>
);
}
}
@@ -6,7 +6,8 @@ export default function SettingsAccount() {
<ContentSection
title="Account"
desc="Update your account settings. Set your preferred language and
timezone.">
timezone."
>
<AccountForm />
</ContentSection>
);
@@ -15,18 +15,18 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark"], {
required_error: "Please select a theme."
required_error: "Please select a theme.",
}),
font: z.enum(fonts, {
invalid_type_error: "Select a font",
required_error: "Please select a font."
})
required_error: "Please select a font.",
}),
});
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
@@ -38,12 +38,12 @@ export function AppearanceForm() {
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: theme as "light" | "dark",
font
font,
};
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues
defaultValues,
});
function onSubmit(data: AppearanceFormValues) {
@@ -56,7 +56,7 @@ export function AppearanceForm() {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
}
@@ -74,9 +74,10 @@ export function AppearanceForm() {
<select
className={cn(
buttonVariants({ variant: "outline" }),
"w-[200px] appearance-none font-normal capitalize"
"w-[200px] appearance-none font-normal capitalize",
)}
{...field}>
{...field}
>
{fonts.map((font) => (
<option key={font} value={font}>
{font}
@@ -106,7 +107,8 @@ export function AppearanceForm() {
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="grid max-w-md grid-cols-2 gap-8 pt-2">
className="grid max-w-md grid-cols-2 gap-8 pt-2"
>
<FormItem>
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
<FormControl>
@@ -6,7 +6,8 @@ export default function SettingsAppearance() {
<ContentSection
title="Appearance"
desc="Customize the appearance of the app. Automatically switch between day
and night themes.">
and night themes."
>
<AppearanceForm />
</ContentSection>
);
@@ -10,7 +10,7 @@ interface ContentSectionProps {
export default function ContentSection({
title,
desc,
children
children,
}: ContentSectionProps) {
return (
<div className="flex flex-1 flex-col">
@@ -9,7 +9,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
SelectValue,
} from "@/components/ui/select";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
@@ -57,13 +57,15 @@ export default function SidebarNav({
<ScrollArea
orientation="horizontal"
type="always"
className="hidden w-full min-w-40 bg-background px-1 py-2 md:block">
className="hidden w-full min-w-40 bg-background px-1 py-2 md:block"
>
<nav
className={cn(
"flex space-x-2 py-1 lg:flex-col lg:space-x-0 lg:space-y-1",
className
className,
)}
{...props}>
{...props}
>
{items.map((item) => (
<Link
key={item.href}
@@ -73,8 +75,9 @@ export default function SidebarNav({
pathname === item.href
? "bg-muted hover:bg-muted"
: "hover:bg-transparent hover:underline",
"justify-start"
)}>
"justify-start",
)}
>
<span className="mr-2">{item.icon}</span>
{item.title}
</Link>
@@ -11,53 +11,53 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
const items = [
{
id: "recents",
label: "Recents"
label: "Recents",
},
{
id: "home",
label: "Home"
label: "Home",
},
{
id: "applications",
label: "Applications"
label: "Applications",
},
{
id: "desktop",
label: "Desktop"
label: "Desktop",
},
{
id: "downloads",
label: "Downloads"
label: "Downloads",
},
{
id: "documents",
label: "Documents"
}
label: "Documents",
},
] as const;
const displayFormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "You have to select at least one item."
})
message: "You have to select at least one item.",
}),
});
type DisplayFormValues = z.infer<typeof displayFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<DisplayFormValues> = {
items: ["recents", "home"]
items: ["recents", "home"],
};
export function DisplayForm() {
const form = useForm<DisplayFormValues>({
resolver: zodResolver(displayFormSchema),
defaultValues
defaultValues,
});
function onSubmit(data: DisplayFormValues) {
@@ -67,7 +67,7 @@ export function DisplayForm() {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
}
@@ -94,7 +94,8 @@ export function DisplayForm() {
return (
<FormItem
key={item.id}
className="flex flex-row items-start space-x-3 space-y-0">
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
@@ -103,8 +104,8 @@ export function DisplayForm() {
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
(value) => value !== item.id,
),
);
}}
/>
@@ -5,7 +5,8 @@ export default function SettingsDisplay() {
return (
<ContentSection
title="Display"
desc="Turn items on or off to control what's displayed in the app.">
desc="Turn items on or off to control what's displayed in the app."
>
<DisplayForm />
</ContentSection>
);
+7 -7
View File
@@ -4,7 +4,7 @@ import {
IconLock,
IconNotification,
IconPalette,
IconUser
IconUser,
} from "@tabler/icons-react";
import { Separator } from "@/components/ui/separator";
import { Header } from "@/components/layout/header";
@@ -53,26 +53,26 @@ const sidebarNavItems = [
{
title: "Profile",
icon: <IconUser size={18} />,
href: "/dashboard/settings"
href: "/dashboard/settings",
},
{
title: "Security",
icon: <IconLock size={18} />,
href: "/dashboard/settings/security"
href: "/dashboard/settings/security",
},
{
title: "Appearance",
icon: <IconPalette size={18} />,
href: "/dashboard/settings/appearance"
href: "/dashboard/settings/appearance",
},
{
title: "Notifications",
icon: <IconNotification size={18} />,
href: "/dashboard/settings/notifications"
href: "/dashboard/settings/notifications",
},
{
title: "Display",
icon: <IconBrowserCheck size={18} />,
href: "/dashboard/settings/display"
}
href: "/dashboard/settings/display",
},
];
@@ -5,7 +5,8 @@ export default function SettingsNotifications() {
return (
<ContentSection
title="Notifications"
desc="Configure how you receive notifications.">
desc="Configure how you receive notifications."
>
<NotificationsForm />
</ContentSection>
);
@@ -12,20 +12,20 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
const notificationsFormSchema = z.object({
type: z.enum(["all", "mentions", "none"], {
required_error: "You need to select a notification type."
required_error: "You need to select a notification type.",
}),
mobile: z.boolean().default(false).optional(),
communication_emails: z.boolean().default(false).optional(),
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean()
security_emails: z.boolean(),
});
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>;
@@ -35,13 +35,13 @@ const defaultValues: Partial<NotificationsFormValues> = {
communication_emails: false,
marketing_emails: false,
social_emails: true,
security_emails: true
security_emails: true,
};
export function NotificationsForm() {
const form = useForm<NotificationsFormValues>({
resolver: zodResolver(notificationsFormSchema),
defaultValues
defaultValues,
});
function onSubmit(data: NotificationsFormValues) {
@@ -51,7 +51,7 @@ export function NotificationsForm() {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
}
@@ -68,7 +68,8 @@ export function NotificationsForm() {
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1">
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
@@ -207,7 +208,8 @@ export function NotificationsForm() {
You can manage your mobile notifications in the{" "}
<Link
to="/settings"
className="underline decoration-dashed underline-offset-4 hover:decoration-solid">
className="underline decoration-dashed underline-offset-4 hover:decoration-solid"
>
mobile settings
</Link>{" "}
page.
@@ -5,7 +5,8 @@ export default function SettingsProfile() {
return (
<ContentSection
title="Profile"
desc="This is how others will see you on the site.">
desc="This is how others will see you on the site."
>
<ProfileForm />
</ContentSection>
);
@@ -11,7 +11,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PhoneInput } from "@/components/ui/phone-number-input";
@@ -20,42 +20,42 @@ import useAuth from "@/hooks/useAuth";
const profileFormSchema = z.object({
username: z
.string({
required_error: "Please enter your desired username"
required_error: "Please enter your desired username",
})
.min(3, {
message: "Username must be at least 3 characters."
message: "Username must be at least 3 characters.",
})
.max(64, {
message: "Username must not be longer than 64 characters."
message: "Username must not be longer than 64 characters.",
}),
email: z
.string()
.email({
message: "Please enter your email address."
message: "Please enter your email address.",
})
.email(),
phone_number: z
.string({
required_error: "Please input a valid phone number."
required_error: "Please input a valid phone number.",
})
.min(2, {
message: "Phone number must be at least 2 characters."
message: "Phone number must be at least 2 characters.",
})
.max(16, {
message: "Phone number must not be longer than 16 characters."
message: "Phone number must not be longer than 16 characters.",
}),
first_name: z
.string()
.max(64, {
message: "First name must not be longer than 64 characters."
message: "First name must not be longer than 64 characters.",
})
.optional(),
last_name: z
.string()
.max(64, {
message: "Last name must not be longer than 64 characters."
message: "Last name must not be longer than 64 characters.",
})
.optional()
.optional(),
});
type ProfileFormValues = z.infer<typeof profileFormSchema>;
@@ -68,13 +68,13 @@ export default function ProfileForm() {
email: user?.email ?? "",
phone_number: user?.phone_number ?? "",
first_name: user?.first_name ?? "",
last_name: user?.last_name ?? ""
last_name: user?.last_name ?? "",
};
const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileFormSchema),
defaultValues,
mode: "onChange"
mode: "onChange",
});
function onSubmit(data: ProfileFormValues) {
@@ -85,7 +85,7 @@ export default function ProfileForm() {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
}
@@ -8,7 +8,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { PasswordInput } from "@/components/password-input";
import { toast } from "@/hooks/useToast";
@@ -16,7 +16,7 @@ import { toast } from "@/hooks/useToast";
const passwordChangeSchema = z
.object({
oldPassword: z.string().min(1, {
message: "Please enter your existing password"
message: "Please enter your existing password",
}),
newPassword: z
.string()
@@ -24,25 +24,25 @@ const passwordChangeSchema = z
.min(8, { message: "Password must be at least 8 characters long" })
.max(128, { message: "Password must be at most 128 characters long" })
.refine((password) => /[A-Z]/.test(password), {
message: "Password must contain at least one uppercase letter"
message: "Password must contain at least one uppercase letter",
})
.refine((password) => /[a-z]/.test(password), {
message: "Password must contain at least one lowercase letter"
message: "Password must contain at least one lowercase letter",
})
.refine((password) => /\d/.test(password), {
message: "Password must contain at least one number"
message: "Password must contain at least one number",
})
.refine((password) => /[@$!%*?&]/.test(password), {
message:
"Password must contain at least one special character (@, $, !, %, *, ?, &)"
"Password must contain at least one special character (@, $, !, %, *, ?, &)",
}),
confirmNewPassword: z.string().min(1, {
message: "Please confirm your new password"
})
message: "Please confirm your new password",
}),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: "Passwords don't match.",
path: ["confirmNewPassword"]
path: ["confirmNewPassword"],
});
type PasswordChangeFormValues = z.infer<typeof passwordChangeSchema>;
@@ -53,14 +53,14 @@ export default function PasswordChangeForm() {
defaultValues: {
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
}
confirmNewPassword: "",
},
});
function onSubmit(data: PasswordChangeFormValues) {
toast({
title: "Password Changed (MOCK)",
description: "Your password has been updated."
description: "Your password has been updated.",
});
}
@@ -8,7 +8,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { PhoneInput } from "@/components/ui/phone-number-input";
@@ -30,9 +30,9 @@ const shopAboutSchema = z.object({
city: z.string(),
state: z.string().nullable().optional(),
postal_code: z.string(),
country: z.string()
country: z.string(),
}),
status: z.enum(["active", "inactive", "suspended"])
status: z.enum(["active", "inactive", "suspended"]),
});
type ShopAboutFormValues = z.infer<typeof shopAboutSchema>;
@@ -43,7 +43,7 @@ export function ShopAboutForm() {
const form = useForm<ShopAboutFormValues>({
resolver: zodResolver(shopAboutSchema),
defaultValues: shop ?? undefined,
mode: "onChange"
mode: "onChange",
});
useEffect(() => {
@@ -52,7 +52,7 @@ export function ShopAboutForm() {
function onSubmit(data: ShopAboutFormValues) {
updateShop(data);
toast({title: "Saved shop data"})
toast({ title: "Saved shop data" });
}
if (isLoading) return <div className="p-4">Loading...</div>;
if (!shop) return <div className="p-4">No shop found</div>;
@@ -61,7 +61,8 @@ export function ShopAboutForm() {
<ScrollArea
orientation="horizontal"
type="always"
className="hidden w-full min-w-40 bg-background px-1 md:block">
className="hidden w-full min-w-40 bg-background px-1 md:block"
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Card>
@@ -190,7 +191,11 @@ export function ShopAboutForm() {
<FormItem>
<FormLabel>State/Province</FormLabel>
<FormControl>
<Input placeholder="State" {...field} value={field.value ?? ""} />
<Input
placeholder="State"
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -4,14 +4,14 @@ import {
Select,
SelectContent,
SelectTrigger,
SelectValue
SelectValue,
} from "@/components/ui/select";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { IconBuildingStore, IconClock, IconLink } from "@tabler/icons-react";
@@ -29,7 +29,7 @@ export const columns: ColumnDef<Task>[] = [
/>
),
enableSorting: false,
enableHiding: false
enableHiding: false,
},
{
accessorKey: "id",
@@ -38,7 +38,7 @@ export const columns: ColumnDef<Task>[] = [
),
cell: ({ row }) => <div className="w-[80px]">{row.getValue("id")}</div>,
enableSorting: false,
enableHiding: false
enableHiding: false,
},
{
accessorKey: "title",
@@ -56,7 +56,7 @@ export const columns: ColumnDef<Task>[] = [
</span>
</div>
);
}
},
},
{
accessorKey: "status",
@@ -65,7 +65,7 @@ export const columns: ColumnDef<Task>[] = [
),
cell: ({ row }) => {
const status = statuses.find(
(status) => status.value === row.getValue("status")
(status) => status.value === row.getValue("status"),
);
if (!status) {
@@ -83,7 +83,7 @@ export const columns: ColumnDef<Task>[] = [
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
}
},
},
{
accessorKey: "priority",
@@ -92,7 +92,7 @@ export const columns: ColumnDef<Task>[] = [
),
cell: ({ row }) => {
const priority = priorities.find(
(priority) => priority.value === row.getValue("priority")
(priority) => priority.value === row.getValue("priority"),
);
if (!priority) {
@@ -110,10 +110,10 @@ export const columns: ColumnDef<Task>[] = [
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
}
},
},
{
id: "actions",
cell: ({ row }) => <DataTableRowActions row={row} />
}
cell: ({ row }) => <DataTableRowActions row={row} />,
},
];
@@ -2,7 +2,7 @@ import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon
EyeNoneIcon,
} from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
@@ -12,7 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableColumnHeaderProps<TData, TValue>
@@ -24,7 +24,7 @@ interface DataTableColumnHeaderProps<TData, TValue>
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
@@ -37,7 +37,8 @@ export function DataTableColumnHeader<TData, TValue>({
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent">
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
@@ -11,12 +11,12 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandSeparator
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
@@ -33,7 +33,7 @@ interface DataTableFacetedFilterProps<TData, TValue> {
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
@@ -49,14 +49,16 @@ export function DataTableFacetedFilter<TData, TValue>({
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden">
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal">
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
@@ -66,7 +68,8 @@ export function DataTableFacetedFilter<TData, TValue>({
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal">
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
@@ -95,16 +98,18 @@ export function DataTableFacetedFilter<TData, TValue>({
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(
filterValues.length ? filterValues : undefined
filterValues.length ? filterValues : undefined,
);
}}>
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}>
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
@@ -126,7 +131,8 @@ export function DataTableFacetedFilter<TData, TValue>({
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center">
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
@@ -2,7 +2,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
@@ -11,7 +11,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
SelectValue,
} from "@/components/ui/select";
interface DataTablePaginationProps<TData> {
@@ -19,12 +19,13 @@ interface DataTablePaginationProps<TData> {
}
export function DataTablePagination<TData>({
table
table,
}: DataTablePaginationProps<TData>) {
return (
<div
className="flex items-center justify-between overflow-clip px-2"
style={{ overflowClipMargin: 1 }}>
style={{ overflowClipMargin: 1 }}
>
<div className="hidden flex-1 text-sm text-muted-foreground sm:block">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
@@ -36,7 +37,8 @@ export function DataTablePagination<TData>({
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}>
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
@@ -58,7 +60,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}>
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
@@ -66,7 +69,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}>
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
@@ -74,7 +78,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}>
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
@@ -82,7 +87,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}>
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
@@ -13,7 +13,7 @@ import {
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTasks } from "../context/tasks-context";
import { labels } from "../data/data";
@@ -24,7 +24,7 @@ interface DataTableRowActionsProps<TData> {
}
export function DataTableRowActions<TData>({
row
row,
}: DataTableRowActionsProps<TData>) {
const task = taskSchema.parse(row.original);
@@ -35,7 +35,8 @@ export function DataTableRowActions<TData>({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted">
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
@@ -45,7 +46,8 @@ export function DataTableRowActions<TData>({
onClick={() => {
setCurrentRow(task);
setOpen("update");
}}>
}}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem disabled>Make a copy</DropdownMenuItem>
@@ -68,7 +70,8 @@ export function DataTableRowActions<TData>({
onClick={() => {
setCurrentRow(task);
setOpen("delete");
}}>
}}
>
Delete
<DropdownMenuShortcut>
<IconTrash size={16} />
@@ -11,7 +11,7 @@ interface DataTableToolbarProps<TData> {
}
export function DataTableToolbar<TData>({
table
table,
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
@@ -46,7 +46,8 @@ export function DataTableToolbar<TData>({
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3">
className="h-8 px-2 lg:px-3"
>
Reset
<Cross2Icon className="ml-2 h-4 w-4" />
</Button>
@@ -7,7 +7,7 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface DataTableViewOptionsProps<TData> {
@@ -15,7 +15,7 @@ interface DataTableViewOptionsProps<TData> {
}
export function DataTableViewOptions<TData>({
table
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu modal={false}>
@@ -23,7 +23,8 @@ export function DataTableViewOptions<TData>({
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex">
className="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
View
</Button>
@@ -35,7 +36,7 @@ export function DataTableViewOptions<TData>({
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
typeof column.accessorFn !== "undefined" && column.getCanHide(),
)
.map((column) => {
return (
@@ -43,7 +44,8 @@ export function DataTableViewOptions<TData>({
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}>
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
@@ -11,7 +11,7 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
useReactTable,
} from "@tanstack/react-table";
import {
Table,
@@ -19,7 +19,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { DataTablePagination } from "../components/data-table-pagination";
import { DataTableToolbar } from "../components/data-table-toolbar";
@@ -31,13 +31,13 @@ interface DataTableProps<TData, TValue> {
export function DataTable<TData, TValue>({
columns,
data
data,
}: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = React.useState({});
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
[],
);
const [sorting, setSorting] = React.useState<SortingState>([]);
@@ -48,7 +48,7 @@ export function DataTable<TData, TValue>({
sorting,
columnVisibility,
rowSelection,
columnFilters
columnFilters,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
@@ -60,7 +60,7 @@ export function DataTable<TData, TValue>({
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues()
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
@@ -78,7 +78,7 @@ export function DataTable<TData, TValue>({
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
header.getContext(),
)}
</TableHead>
);
@@ -91,12 +91,13 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}>
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
cell.getContext(),
)}
</TableCell>
))}
@@ -106,7 +107,8 @@ export function DataTable<TData, TValue>({
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center">
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
@@ -57,7 +57,7 @@ export function TasksDialogs() {
{JSON.stringify(currentRow, null, 2)}
</code>
</pre>
)
),
});
}}
className="max-w-md"
@@ -10,7 +10,7 @@ import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
@@ -18,7 +18,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
@@ -26,12 +26,12 @@ const formSchema = z.object({
file: z
.instanceof(FileList)
.refine((files) => files.length > 0, {
message: "Please upload a file"
message: "Please upload a file",
})
.refine(
(files) => ["text/csv"].includes(files?.[0]?.type),
"Please upload csv format."
)
"Please upload csv format.",
),
});
interface Props {
@@ -42,7 +42,7 @@ interface Props {
export function TasksImportDialog({ open, onOpenChange }: Props) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { file: undefined }
defaultValues: { file: undefined },
});
const fileRef = form.register("file");
@@ -54,7 +54,7 @@ export function TasksImportDialog({ open, onOpenChange }: Props) {
const fileDetails = {
name: file[0].name,
size: file[0].size,
type: file[0].type
type: file[0].type,
};
toast({
title: "You have imported the following file:",
@@ -64,7 +64,7 @@ export function TasksImportDialog({ open, onOpenChange }: Props) {
{JSON.stringify(fileDetails, null, 2)}
</code>
</pre>
)
),
});
}
onOpenChange(false);
@@ -76,7 +76,8 @@ export function TasksImportDialog({ open, onOpenChange }: Props) {
onOpenChange={(val) => {
onOpenChange(val);
form.reset();
}}>
}}
>
<DialogContent className="gap-2 sm:max-w-sm">
<DialogHeader className="text-left">
<DialogTitle>Import Tasks</DialogTitle>
@@ -9,7 +9,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
@@ -20,7 +20,7 @@ import {
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle
SheetTitle,
} from "@/components/ui/sheet";
import { SelectDropdown } from "@/components/select-dropdown";
import { Task } from "../data/schema";
@@ -35,7 +35,7 @@ const formSchema = z.object({
title: z.string().min(1, "Title is required."),
status: z.string().min(1, "Please select a status."),
label: z.string().min(1, "Please select a label."),
priority: z.string().min(1, "Please choose a priority.")
priority: z.string().min(1, "Please choose a priority."),
});
type TasksForm = z.infer<typeof formSchema>;
@@ -48,8 +48,8 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
title: "",
status: "",
label: "",
priority: ""
}
priority: "",
},
});
const onSubmit = (data: TasksForm) => {
@@ -62,7 +62,7 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
)
),
});
};
@@ -72,7 +72,8 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
onOpenChange={(v) => {
onOpenChange(v);
form.reset();
}}>
}}
>
<SheetContent className="flex flex-col">
<SheetHeader className="text-left">
<SheetTitle>{isUpdate ? "Update" : "Create"} Task</SheetTitle>
@@ -87,7 +88,8 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
<form
id="tasks-form"
onSubmit={form.handleSubmit(onSubmit)}
className="flex-1 space-y-5">
className="flex-1 space-y-5"
>
<FormField
control={form.control}
name="title"
@@ -116,7 +118,7 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
{ label: "Backlog", value: "backlog" },
{ label: "Todo", value: "todo" },
{ label: "Canceled", value: "canceled" },
{ label: "Done", value: "done" }
{ label: "Done", value: "done" },
]}
/>
<FormMessage />
@@ -133,7 +135,8 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1">
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="documentation" />
@@ -170,7 +173,8 @@ export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1">
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="high" />
@@ -9,7 +9,8 @@ export function TasksPrimaryButtons() {
<Button
variant="outline"
className="space-x-1"
onClick={() => setOpen("import")}>
onClick={() => setOpen("import")}
>
<span>Import</span> <IconDownload size={18} />
</Button>
<Button className="space-x-1" onClick={() => setOpen("create")}>
+15 -15
View File
@@ -6,66 +6,66 @@ import {
IconCircleCheck,
IconCircleX,
IconExclamationCircle,
IconStopwatch
IconStopwatch,
} from "@tabler/icons-react";
export const labels = [
{
value: "bug",
label: "Bug"
label: "Bug",
},
{
value: "feature",
label: "Feature"
label: "Feature",
},
{
value: "documentation",
label: "Documentation"
}
label: "Documentation",
},
];
export const statuses = [
{
value: "backlog",
label: "Backlog",
icon: IconExclamationCircle
icon: IconExclamationCircle,
},
{
value: "todo",
label: "Todo",
icon: IconCircle
icon: IconCircle,
},
{
value: "in progress",
label: "In Progress",
icon: IconStopwatch
icon: IconStopwatch,
},
{
value: "done",
label: "Done",
icon: IconCircleCheck
icon: IconCircleCheck,
},
{
value: "canceled",
label: "Canceled",
icon: IconCircleX
}
icon: IconCircleX,
},
];
export const priorities = [
{
label: "Low",
value: "low",
icon: IconArrowDown
icon: IconArrowDown,
},
{
label: "Medium",
value: "medium",
icon: IconArrowRight
icon: IconArrowRight,
},
{
label: "High",
value: "high",
icon: IconArrowUp
}
icon: IconArrowUp,
},
];
+1 -1
View File
@@ -7,7 +7,7 @@ export const taskSchema = z.object({
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
priority: z.string(),
});
export type Task = z.infer<typeof taskSchema>;
+101 -101
View File
@@ -5,7 +5,7 @@ export const tasks = [
"You can't compress the program without quantifying the open-source SSD pixel!",
status: "in progress",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-7878",
@@ -13,14 +13,14 @@ export const tasks = [
"Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
status: "backlog",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-7839",
title: "We need to bypass the neural TCP card!",
status: "todo",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-5562",
@@ -28,7 +28,7 @@ export const tasks = [
"The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!",
status: "backlog",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-8686",
@@ -36,7 +36,7 @@ export const tasks = [
"I'll parse the wireless SSL protocol, that should driver the API panel!",
status: "canceled",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-1280",
@@ -44,7 +44,7 @@ export const tasks = [
"Use the digital TLS panel, then you can transmit the haptic system!",
status: "done",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-7262",
@@ -52,7 +52,7 @@ export const tasks = [
"The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!",
status: "done",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-1138",
@@ -60,14 +60,14 @@ export const tasks = [
"Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
status: "in progress",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-7184",
title: "We need to program the back-end THX pixel!",
status: "todo",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-5160",
@@ -75,7 +75,7 @@ export const tasks = [
"Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
status: "in progress",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-5618",
@@ -83,7 +83,7 @@ export const tasks = [
"Generating the driver won't do anything, we need to index the online SSL application!",
status: "done",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-6699",
@@ -91,14 +91,14 @@ export const tasks = [
"I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
status: "backlog",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-2858",
title: "We need to override the online UDP bus!",
status: "backlog",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9864",
@@ -106,14 +106,14 @@ export const tasks = [
"I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
status: "done",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-8404",
title: "We need to generate the virtual HEX alarm!",
status: "in progress",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-5365",
@@ -121,7 +121,7 @@ export const tasks = [
"Backing up the pixel won't do anything, we need to transmit the primary IB array!",
status: "in progress",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-1780",
@@ -129,7 +129,7 @@ export const tasks = [
"The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!",
status: "todo",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-6938",
@@ -137,14 +137,14 @@ export const tasks = [
"Use the redundant SCSI application, then you can hack the optical alarm!",
status: "todo",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-9885",
title: "We need to compress the auxiliary VGA driver!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-3216",
@@ -152,7 +152,7 @@ export const tasks = [
"Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
status: "backlog",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9285",
@@ -160,7 +160,7 @@ export const tasks = [
"The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!",
status: "todo",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-1024",
@@ -168,7 +168,7 @@ export const tasks = [
"Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
status: "in progress",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-7068",
@@ -176,7 +176,7 @@ export const tasks = [
"You can't generate the capacitor without indexing the wireless HEX pixel!",
status: "canceled",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-6502",
@@ -184,14 +184,14 @@ export const tasks = [
"Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
status: "todo",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-5326",
title: "We need to hack the redundant UTF8 transmitter!",
status: "todo",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-6274",
@@ -199,7 +199,7 @@ export const tasks = [
"Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
status: "canceled",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-1571",
@@ -207,7 +207,7 @@ export const tasks = [
"I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
status: "in progress",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9518",
@@ -215,7 +215,7 @@ export const tasks = [
"Compressing the interface won't do anything, we need to compress the online SDD matrix!",
status: "canceled",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-5581",
@@ -223,7 +223,7 @@ export const tasks = [
"I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
status: "backlog",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-2197",
@@ -231,14 +231,14 @@ export const tasks = [
"Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
status: "todo",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-8484",
title: "We need to parse the solid state UDP firewall!",
status: "in progress",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-9892",
@@ -246,14 +246,14 @@ export const tasks = [
"If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!",
status: "done",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-9616",
title: "We need to synthesize the cross-platform ASCII pixel!",
status: "in progress",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9744",
@@ -261,7 +261,7 @@ export const tasks = [
"Use the back-end IP card, then you can input the solid state hard drive!",
status: "done",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-1376",
@@ -269,7 +269,7 @@ export const tasks = [
"Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
status: "backlog",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-7382",
@@ -277,7 +277,7 @@ export const tasks = [
"If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!",
status: "todo",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-2290",
@@ -285,7 +285,7 @@ export const tasks = [
"I'll compress the virtual JSON panel, that should application the UTF8 bus!",
status: "canceled",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-1533",
@@ -293,7 +293,7 @@ export const tasks = [
"You can't input the firewall without overriding the wireless TCP firewall!",
status: "done",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-4920",
@@ -301,7 +301,7 @@ export const tasks = [
"Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
status: "in progress",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-5168",
@@ -309,14 +309,14 @@ export const tasks = [
"If we synthesize the bus, we can get to the IP panel through the virtual TLS array!",
status: "in progress",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-7103",
title: "We need to parse the multi-byte EXE bandwidth!",
status: "canceled",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-4314",
@@ -324,7 +324,7 @@ export const tasks = [
"If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!",
status: "in progress",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-3415",
@@ -332,7 +332,7 @@ export const tasks = [
"Use the cross-platform XML application, then you can quantify the solid state feed!",
status: "todo",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-8339",
@@ -340,7 +340,7 @@ export const tasks = [
"Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
status: "in progress",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-6995",
@@ -348,7 +348,7 @@ export const tasks = [
"Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
status: "todo",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-8053",
@@ -356,7 +356,7 @@ export const tasks = [
"If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!",
status: "todo",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-4336",
@@ -364,7 +364,7 @@ export const tasks = [
"If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!",
status: "todo",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-8790",
@@ -372,7 +372,7 @@ export const tasks = [
"I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
status: "done",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-8980",
@@ -380,14 +380,14 @@ export const tasks = [
"Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
status: "canceled",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-7342",
title: "Use the neural CLI card, then you can parse the online port!",
status: "backlog",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-5608",
@@ -395,7 +395,7 @@ export const tasks = [
"I'll hack the haptic SSL program, that should bus the UDP transmitter!",
status: "canceled",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-1606",
@@ -403,7 +403,7 @@ export const tasks = [
"I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
status: "done",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-7872",
@@ -411,7 +411,7 @@ export const tasks = [
"Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
status: "canceled",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-4167",
@@ -419,7 +419,7 @@ export const tasks = [
"Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
status: "canceled",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9581",
@@ -427,14 +427,14 @@ export const tasks = [
"You can't index the port without hacking the cross-platform XSS monitor!",
status: "backlog",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-8806",
title: "We need to bypass the back-end SSL panel!",
status: "done",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-6542",
@@ -442,7 +442,7 @@ export const tasks = [
"Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
status: "done",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-6806",
@@ -450,14 +450,14 @@ export const tasks = [
"The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!",
status: "canceled",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-9549",
title: "You can't bypass the bus without connecting the neural JBOD bus!",
status: "todo",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-1075",
@@ -465,7 +465,7 @@ export const tasks = [
"Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
status: "done",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-1427",
@@ -473,7 +473,7 @@ export const tasks = [
"Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
status: "done",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-1907",
@@ -481,7 +481,7 @@ export const tasks = [
"Hacking the circuit won't do anything, we need to back up the online DRAM system!",
status: "todo",
label: "documentation",
priority: "high"
priority: "high",
},
{
id: "TASK-4309",
@@ -489,7 +489,7 @@ export const tasks = [
"If we generate the system, we can get to the TCP sensor through the optical GB pixel!",
status: "backlog",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3973",
@@ -497,7 +497,7 @@ export const tasks = [
"I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
status: "todo",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-7962",
@@ -505,7 +505,7 @@ export const tasks = [
"Use the wireless RAM program, then you can hack the cross-platform feed!",
status: "canceled",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-3360",
@@ -513,7 +513,7 @@ export const tasks = [
"You can't quantify the program without synthesizing the neural OCR interface!",
status: "done",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-9887",
@@ -521,7 +521,7 @@ export const tasks = [
"Use the auxiliary ASCII sensor, then you can connect the solid state port!",
status: "backlog",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3649",
@@ -529,7 +529,7 @@ export const tasks = [
"I'll input the virtual USB system, that should circuit the DNS monitor!",
status: "in progress",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3586",
@@ -537,7 +537,7 @@ export const tasks = [
"If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!",
status: "in progress",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-5150",
@@ -545,7 +545,7 @@ export const tasks = [
"I'll hack the wireless XSS port, that should transmitter the IP interface!",
status: "canceled",
label: "feature",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3652",
@@ -553,7 +553,7 @@ export const tasks = [
"The SQL interface is down, override the optical bus so we can program the ASCII interface!",
status: "backlog",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-6884",
@@ -561,14 +561,14 @@ export const tasks = [
"Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
status: "canceled",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-1591",
title: "We need to connect the mobile XSS driver!",
status: "in progress",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-3802",
@@ -576,7 +576,7 @@ export const tasks = [
"Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
status: "in progress",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-7253",
@@ -584,14 +584,14 @@ export const tasks = [
"Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-9739",
title: "We need to hack the multi-byte HDD bus!",
status: "done",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-4424",
@@ -599,7 +599,7 @@ export const tasks = [
"Try to hack the HEX alarm, maybe it will connect the optical pixel!",
status: "in progress",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3922",
@@ -607,7 +607,7 @@ export const tasks = [
"You can't back up the capacitor without generating the wireless PCI program!",
status: "backlog",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-4921",
@@ -615,14 +615,14 @@ export const tasks = [
"I'll index the open-source IP feed, that should system the GB application!",
status: "canceled",
label: "bug",
priority: "low"
priority: "low",
},
{
id: "TASK-5814",
title: "We need to calculate the 1080p AGP feed!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-2645",
@@ -630,7 +630,7 @@ export const tasks = [
"Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
status: "todo",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-4535",
@@ -638,14 +638,14 @@ export const tasks = [
"Try to copy the JSON circuit, maybe it will connect the wireless feed!",
status: "in progress",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-4463",
title: "We need to copy the solid state AGP monitor!",
status: "done",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-9745",
@@ -653,7 +653,7 @@ export const tasks = [
"If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!",
status: "canceled",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-2080",
@@ -661,7 +661,7 @@ export const tasks = [
"If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!",
status: "todo",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3838",
@@ -669,14 +669,14 @@ export const tasks = [
"I'll bypass the online TCP application, that should panel the AGP system!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-1340",
title: "We need to navigate the virtual PNG circuit!",
status: "todo",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-6665",
@@ -684,7 +684,7 @@ export const tasks = [
"If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!",
status: "canceled",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-7585",
@@ -692,28 +692,28 @@ export const tasks = [
"If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!",
status: "backlog",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-6319",
title: "We need to copy the multi-byte SCSI program!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-4369",
title: "Try to input the SCSI bus, maybe it will generate the 1080p pixel!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-9035",
title: "We need to override the solid state PNG array!",
status: "canceled",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-3970",
@@ -721,7 +721,7 @@ export const tasks = [
"You can't index the transmitter without quantifying the haptic ASCII card!",
status: "todo",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-4473",
@@ -729,7 +729,7 @@ export const tasks = [
"You can't bypass the protocol without overriding the neural RSS program!",
status: "todo",
label: "documentation",
priority: "low"
priority: "low",
},
{
id: "TASK-4136",
@@ -737,7 +737,7 @@ export const tasks = [
"You can't hack the hard drive without hacking the primary JSON program!",
status: "canceled",
label: "bug",
priority: "medium"
priority: "medium",
},
{
id: "TASK-3939",
@@ -745,7 +745,7 @@ export const tasks = [
"Use the back-end SQL firewall, then you can connect the neural hard drive!",
status: "done",
label: "feature",
priority: "low"
priority: "low",
},
{
id: "TASK-2007",
@@ -753,7 +753,7 @@ export const tasks = [
"I'll input the back-end USB protocol, that should bandwidth the PCI system!",
status: "backlog",
label: "bug",
priority: "high"
priority: "high",
},
{
id: "TASK-7516",
@@ -761,7 +761,7 @@ export const tasks = [
"Use the primary SQL program, then you can generate the auxiliary transmitter!",
status: "done",
label: "documentation",
priority: "medium"
priority: "medium",
},
{
id: "TASK-6906",
@@ -769,7 +769,7 @@ export const tasks = [
"Try to back up the DRAM system, maybe it will reboot the online transmitter!",
status: "done",
label: "feature",
priority: "high"
priority: "high",
},
{
id: "TASK-5207",
@@ -777,6 +777,6 @@ export const tasks = [
"The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!",
status: "in progress",
label: "bug",
priority: "low"
}
priority: "low",
},
];
@@ -2,7 +2,7 @@ import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon
EyeNoneIcon,
} from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
@@ -12,7 +12,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DataTableColumnHeaderProps<TData, TValue>
@@ -24,7 +24,7 @@ interface DataTableColumnHeaderProps<TData, TValue>
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
@@ -37,7 +37,8 @@ export function DataTableColumnHeader<TData, TValue>({
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent">
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
@@ -11,12 +11,12 @@ import {
CommandInput,
CommandItem,
CommandList,
CommandSeparator
CommandSeparator,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
@@ -33,7 +33,7 @@ interface DataTableFacetedFilterProps<TData, TValue> {
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues();
const selectedValues = new Set(column?.getFilterValue() as string[]);
@@ -48,14 +48,16 @@ export function DataTableFacetedFilter<TData, TValue>({
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden">
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal">
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
@@ -65,7 +67,8 @@ export function DataTableFacetedFilter<TData, TValue>({
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal">
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
@@ -94,16 +97,18 @@ export function DataTableFacetedFilter<TData, TValue>({
}
const filterValues = Array.from(selectedValues);
column?.setFilterValue(
filterValues.length ? filterValues : undefined
filterValues.length ? filterValues : undefined,
);
}}>
}}
>
<div
className={cn(
"flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}>
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
@@ -125,7 +130,8 @@ export function DataTableFacetedFilter<TData, TValue>({
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center">
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
@@ -2,7 +2,7 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
@@ -11,7 +11,7 @@ import {
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
SelectValue,
} from "@/components/ui/select";
interface DataTablePaginationProps<TData> {
@@ -19,12 +19,13 @@ interface DataTablePaginationProps<TData> {
}
export function DataTablePagination<TData>({
table
table,
}: DataTablePaginationProps<TData>) {
return (
<div
className="flex items-center justify-between overflow-clip px-2"
style={{ overflowClipMargin: 1 }}>
style={{ overflowClipMargin: 1 }}
>
<div className="hidden flex-1 text-sm text-muted-foreground sm:block">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
@@ -36,7 +37,8 @@ export function DataTablePagination<TData>({
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}>
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
@@ -58,7 +60,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}>
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
</Button>
@@ -66,7 +69,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}>
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
@@ -74,7 +78,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}>
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRightIcon className="h-4 w-4" />
</Button>
@@ -82,7 +87,8 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}>
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<DoubleArrowRightIcon className="h-4 w-4" />
</Button>
@@ -8,7 +8,7 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useUsers } from "../context/users-context";
import { User } from "../data/schema";
@@ -25,7 +25,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted">
className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
@@ -35,7 +36,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
onClick={() => {
setCurrentRow(row.original);
setOpen("edit");
}}>
}}
>
Edit
<DropdownMenuShortcut>
<IconEdit size={16} />
@@ -47,7 +49,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
setCurrentRow(row.original);
setOpen("delete");
}}
className="!text-red-500">
className="!text-red-500"
>
Delete
<DropdownMenuShortcut>
<IconTrash size={16} />
@@ -11,7 +11,7 @@ interface DataTableToolbarProps<TData> {
}
export function DataTableToolbar<TData>({
table
table,
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
@@ -37,7 +37,7 @@ export function DataTableToolbar<TData>({
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
{ label: "Invited", value: "invited" },
{ label: "Suspended", value: "suspended" }
{ label: "Suspended", value: "suspended" },
]}
/>
)}
@@ -53,7 +53,8 @@ export function DataTableToolbar<TData>({
<Button
variant="ghost"
onClick={() => table.resetColumnFilters()}
className="h-8 px-2 lg:px-3">
className="h-8 px-2 lg:px-3"
>
Reset
<Cross2Icon className="ml-2 h-4 w-4" />
</Button>
@@ -7,7 +7,7 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
interface DataTableViewOptionsProps<TData> {
@@ -15,7 +15,7 @@ interface DataTableViewOptionsProps<TData> {
}
export function DataTableViewOptions<TData>({
table
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu modal={false}>
@@ -23,7 +23,8 @@ export function DataTableViewOptions<TData>({
<Button
variant="outline"
size="sm"
className="ml-auto hidden h-8 lg:flex">
className="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon className="mr-2 h-4 w-4" />
View
</Button>
@@ -35,7 +36,7 @@ export function DataTableViewOptions<TData>({
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
typeof column.accessorFn !== "undefined" && column.getCanHide(),
)
.map((column) => {
return (
@@ -43,7 +44,8 @@ export function DataTableViewOptions<TData>({
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}>
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
);
@@ -11,7 +11,7 @@ import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
@@ -19,7 +19,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -27,6 +27,9 @@ import { PasswordInput } from "@/components/password-input";
import { SelectDropdown } from "@/components/select-dropdown";
import { userTypes } from "../data/data";
import { User } from "../data/schema";
import { useUser } from "@/hooks/useUser";
import { UserRole } from "@/api/mock/models";
import { useShop } from "@/hooks/useShop";
const formSchema = z
.object({
@@ -41,7 +44,7 @@ const formSchema = z
password: z.string().transform((pwd) => pwd.trim()),
role: z.string().min(1, { message: "Role is required." }),
confirmPassword: z.string().transform((pwd) => pwd.trim()),
isEdit: z.boolean()
isEdit: z.boolean(),
})
.superRefine(({ isEdit, password, confirmPassword }, ctx) => {
if (!isEdit || (isEdit && password !== "")) {
@@ -49,7 +52,7 @@ const formSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password is required.",
path: ["password"]
path: ["password"],
});
}
@@ -57,7 +60,7 @@ const formSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password must be at least 8 characters long.",
path: ["password"]
path: ["password"],
});
}
@@ -65,7 +68,7 @@ const formSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password must contain at least one lowercase letter.",
path: ["password"]
path: ["password"],
});
}
@@ -73,7 +76,7 @@ const formSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password must contain at least one number.",
path: ["password"]
path: ["password"],
});
}
@@ -81,7 +84,7 @@ const formSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords don't match.",
path: ["confirmPassword"]
path: ["confirmPassword"],
});
}
}
@@ -95,6 +98,8 @@ interface Props {
}
export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
const { createUser, updateUser } = useUser(currentRow?.id);
const { shop } = useShop();
const isEdit = !!currentRow;
const form = useForm<UserForm>({
resolver: zodResolver(formSchema),
@@ -103,7 +108,7 @@ export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
...currentRow,
password: "",
confirmPassword: "",
isEdit
isEdit,
}
: {
firstName: "",
@@ -114,21 +119,50 @@ export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
phoneNumber: "",
password: "",
confirmPassword: "",
isEdit
}
isEdit,
},
});
const onSubmit = (values: UserForm) => {
form.reset();
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
)
});
onOpenChange(false);
try {
if (isEdit) {
updateUser.mutate({
...values,
first_name: values.firstName,
last_name: values.lastName,
username: values.username,
email: values.email,
phone_number: values.phoneNumber,
user_role: values.role as UserRole,
});
} else {
createUser.mutate({
uuid: crypto.randomUUID(),
shop_id: shop?.id ?? 0,
user_role: values.role as UserRole,
username: values.username,
email: values.email,
password: values.password,
first_name: values.firstName,
last_name: values.lastName,
phone_number: values.phoneNumber,
status: "owner",
});
}
toast({
title: isEdit ? "User updated" : "User created",
description: `${values.firstName} ${values.lastName} (${values.email})`,
});
form.reset();
onOpenChange(false);
} catch (err) {
toast({
title: "An error occurred",
description: (err as Error).message,
variant: "destructive",
});
}
};
const isPasswordTouched = !!form.formState.dirtyFields.password;
@@ -139,7 +173,8 @@ export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
onOpenChange={(state) => {
form.reset();
onOpenChange(state);
}}>
}}
>
<DialogContent className="sm:max-w-lg">
<DialogHeader className="text-left">
<DialogTitle>{isEdit ? "Edit User" : "Add New User"}</DialogTitle>
@@ -153,7 +188,8 @@ export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
<form
id="user-form"
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4 p-0.5">
className="space-y-4 p-0.5"
>
<FormField
control={form.control}
name="firstName"
@@ -266,7 +302,7 @@ export function UsersActionDialog({ currentRow, open, onOpenChange }: Props) {
className="col-span-4"
items={userTypes.map(({ label, value }) => ({
label,
value
value,
}))}
/>
<FormMessage className="col-span-4 col-start-3" />
@@ -1,12 +1,12 @@
import { ColumnDef } from "@tanstack/react-table";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import LongText from "@/components/long-text";
import { callTypes, userTypes } from "../data/data";
import { User } from "../data/schema";
import { DataTableColumnHeader } from "./data-table-column-header";
import { DataTableRowActions } from "./data-table-row-actions";
import { Badge } from "@/components/ui/badge";
export const columns: ColumnDef<User>[] = [
{
@@ -25,8 +25,8 @@ export const columns: ColumnDef<User>[] = [
meta: {
className: cn(
"sticky md:table-cell left-0 z-10 rounded-tl",
"bg-background transition-colors duration-200 group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted"
)
"bg-background transition-colors duration-200 group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
),
},
cell: ({ row }) => (
<Checkbox
@@ -37,7 +37,7 @@ export const columns: ColumnDef<User>[] = [
/>
),
enableSorting: false,
enableHiding: false
enableHiding: false,
},
{
accessorKey: "username",
@@ -51,22 +51,10 @@ export const columns: ColumnDef<User>[] = [
className: cn(
"drop-shadow-[0_1px_2px_rgb(0_0_0_/_0.1)] dark:drop-shadow-[0_1px_2px_rgb(255_255_255_/_0.1)] lg:drop-shadow-none",
"bg-background transition-colors duration-200 group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
"sticky left-6 md:table-cell"
)
"sticky left-6 md:table-cell",
),
},
enableHiding: false
},
{
id: "fullName",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const { firstName, lastName } = row.original;
const fullName = `${firstName} ${lastName}`;
return <LongText className="max-w-36">{fullName}</LongText>;
},
meta: { className: "w-36" }
enableHiding: false,
},
{
accessorKey: "email",
@@ -75,37 +63,39 @@ export const columns: ColumnDef<User>[] = [
),
cell: ({ row }) => (
<div className="w-fit text-nowrap">{row.getValue("email")}</div>
)
),
},
{
accessorKey: "phoneNumber",
id: "fullName",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Name" />
),
cell: ({ row }) => {
const { first_name: firstName, last_name: lastName } = row.original;
if (firstName == undefined || lastName == undefined) {
return (
<LongText className="max-w-36 text-muted select-none">
{"No name available"}
</LongText>
);
}
const fullName = `${firstName} ${lastName}`;
return <LongText className="max-w-36">{fullName}</LongText>;
},
meta: { className: "w-36" },
},
{
accessorKey: "Phone",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Phone Number" />
),
cell: ({ row }) => <div>{row.getValue("phoneNumber")}</div>,
enableSorting: false
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const { status } = row.original;
const badgeColor = callTypes.get(status);
return (
<div className="flex space-x-2">
<Badge variant="outline" className={cn("capitalize", badgeColor)}>
{row.getValue("status")}
</Badge>
</div>
);
const { phone_number } = row.original;
return <div>{phone_number}</div>;
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
enableHiding: false,
enableSorting: false
enableSorting: false,
},
{
accessorKey: "role",
@@ -113,19 +103,22 @@ export const columns: ColumnDef<User>[] = [
<DataTableColumnHeader column={column} title="Role" />
),
cell: ({ row }) => {
const { role } = row.original;
const { user_role: role } = row.original;
const userType = userTypes.find(({ value }) => value === role);
if (!userType) {
return null;
}
const badgeColor = callTypes.get(role);
return (
<div className="flex items-center gap-x-2">
{userType.icon && (
<userType.icon size={16} className="text-muted-foreground" />
)}
<span className="text-sm capitalize">{row.getValue("role")}</span>
<Badge variant="outline" className={cn("capitalize", badgeColor)}>
{role}
</Badge>
</div>
);
},
@@ -133,10 +126,10 @@ export const columns: ColumnDef<User>[] = [
return value.includes(row.getValue(id));
},
enableSorting: false,
enableHiding: false
enableHiding: false,
},
{
id: "actions",
cell: DataTableRowActions
}
cell: DataTableRowActions,
},
];
@@ -30,7 +30,7 @@ export function UsersDeleteDialog({ open, onOpenChange, currentRow }: Props) {
{JSON.stringify(currentRow, null, 2)}
</code>
</pre>
)
),
});
};
@@ -57,7 +57,7 @@ export function UsersDeleteDialog({ open, onOpenChange, currentRow }: Props) {
<br />
This action will permanently remove the user with the role of{" "}
<span className="font-bold">
{currentRow.role.toUpperCase()}
{currentRow.user_role.toUpperCase()}
</span>{" "}
from the system. This cannot be undone.
</p>
@@ -11,7 +11,7 @@ import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
@@ -19,7 +19,7 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -32,7 +32,7 @@ const formSchema = z.object({
.min(1, { message: "Email is required." })
.email({ message: "Email is invalid." }),
role: z.string().min(1, { message: "Role is required." }),
desc: z.string().optional()
desc: z.string().optional(),
});
type UserInviteForm = z.infer<typeof formSchema>;
@@ -44,7 +44,7 @@ interface Props {
export function UsersInviteDialog({ open, onOpenChange }: Props) {
const form = useForm<UserInviteForm>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", role: "", desc: "" }
defaultValues: { email: "", role: "", desc: "" },
});
const onSubmit = (values: UserInviteForm) => {
@@ -55,7 +55,7 @@ export function UsersInviteDialog({ open, onOpenChange }: Props) {
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(values, null, 2)}</code>
</pre>
)
),
});
onOpenChange(false);
};
@@ -66,7 +66,8 @@ export function UsersInviteDialog({ open, onOpenChange }: Props) {
onOpenChange={(state) => {
form.reset();
onOpenChange(state);
}}>
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader className="text-left">
<DialogTitle className="flex items-center gap-2">
@@ -81,7 +82,8 @@ export function UsersInviteDialog({ open, onOpenChange }: Props) {
<form
id="user-invite-form"
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4">
className="space-y-4"
>
<FormField
control={form.control}
name="email"
@@ -111,7 +113,7 @@ export function UsersInviteDialog({ open, onOpenChange }: Props) {
placeholder="Select a role"
items={userTypes.map(({ label, value }) => ({
label,
value
value,
}))}
/>
<FormMessage />
@@ -9,7 +9,8 @@ export function UsersPrimaryButtons() {
<Button
variant="outline"
className="space-x-1"
onClick={() => setOpen("invite")}>
onClick={() => setOpen("invite")}
>
<span>Invite User</span> <IconMailPlus size={18} />
</Button>
<Button className="space-x-1" onClick={() => setOpen("add")}>
@@ -12,7 +12,7 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
useReactTable,
} from "@tanstack/react-table";
import {
Table,
@@ -20,7 +20,7 @@ import {
TableCell,
TableHead,
TableHeader,
TableRow
TableRow,
} from "@/components/ui/table";
import { User } from "../data/schema";
import { DataTablePagination } from "./data-table-pagination";
@@ -51,7 +51,7 @@ export function UsersTable({ columns, data }: DataTableProps) {
sorting,
columnVisibility,
rowSelection,
columnFilters
columnFilters,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
@@ -63,7 +63,7 @@ export function UsersTable({ columns, data }: DataTableProps) {
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues()
getFacetedUniqueValues: getFacetedUniqueValues(),
});
return (
@@ -79,12 +79,13 @@ export function UsersTable({ columns, data }: DataTableProps) {
<TableHead
key={header.id}
colSpan={header.colSpan}
className={header.column.columnDef.meta?.className ?? ""}>
className={header.column.columnDef.meta?.className ?? ""}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
header.getContext(),
)}
</TableHead>
);
@@ -98,14 +99,16 @@ export function UsersTable({ columns, data }: DataTableProps) {
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="group/row">
className="group/row"
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.className ?? ""}>
className={cell.column.columnDef.meta?.className ?? ""}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
cell.getContext(),
)}
</TableCell>
))}
@@ -115,7 +118,8 @@ export function UsersTable({ columns, data }: DataTableProps) {
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center">
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
+37 -18
View File
@@ -1,40 +1,59 @@
import {
IconCash,
IconShield,
IconUser,
IconUsersGroup,
IconUserShield
IconUserShield,
} from "@tabler/icons-react";
import { UserStatus } from "./schema";
import { UserRole } from "./schema";
export const callTypes = new Map<UserStatus, string>([
["active", "bg-teal-100/30 text-teal-900 dark:text-teal-200 border-teal-200"],
["inactive", "bg-neutral-300/40 border-neutral-300"],
["invited", "bg-sky-200/40 text-sky-900 dark:text-sky-100 border-sky-300"],
export const callTypes = new Map<UserRole, string>([
[
"suspended",
"bg-destructive/10 dark:bg-destructive/50 text-destructive dark:text-primary border-destructive/10"
]
"owner",
"bg-purple-100 text-purple-900 dark:bg-purple-900 dark:text-purple-100 border border-purple-300 dark:border-purple-600",
],
[
"admin",
"bg-emerald-100 text-emerald-900 dark:bg-emerald-900 dark:text-emerald-100 border border-emerald-300 dark:border-emerald-600",
],
[
"manager",
"bg-orange-100 text-orange-900 dark:bg-orange-900 dark:text-orange-100 border border-orange-300 dark:border-orange-600",
],
[
"employee",
"bg-sky-100 text-sky-900 dark:bg-sky-900 dark:text-sky-100 border border-sky-300 dark:border-sky-600",
],
[
"customer",
"bg-neutral-200 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100 border border-neutral-300 dark:border-neutral-600",
],
]);
export const userTypes = [
{
label: "Superadmin",
value: "superadmin",
icon: IconShield
label: "Owner",
value: "owner",
icon: IconUserShield,
},
{
label: "Admin",
value: "admin",
icon: IconUserShield
icon: IconShield,
},
{
label: "Manager",
value: "manager",
icon: IconUsersGroup
icon: IconUsersGroup,
},
{
label: "Cashier",
value: "cashier",
icon: IconCash
}
label: "Employee",
value: "employee",
icon: IconCash,
},
{
label: "Customer",
value: "customer",
icon: IconUser,
},
] as const;
+21 -22
View File
@@ -1,32 +1,31 @@
import { z } from "zod";
const userStatusSchema = z.union([
z.literal("active"),
z.literal("inactive"),
z.literal("invited"),
z.literal("suspended")
]);
export type UserStatus = z.infer<typeof userStatusSchema>;
const userRoleSchema = z.union([
z.literal("superadmin"),
export const userRoleSchema = z.union([
z.literal("owner"),
z.literal("admin"),
z.literal("cashier"),
z.literal("manager")
z.literal("manager"),
z.literal("employee"),
z.literal("customer"),
]);
export type UserRole = z.infer<typeof userRoleSchema>;
const userSchema = z.object({
id: z.string(),
firstName: z.string(),
lastName: z.string(),
export const userSchema = z.object({
id: z.number(),
uuid: z.string().uuid(),
username: z.string(),
email: z.string(),
phoneNumber: z.string(),
status: userStatusSchema,
role: userRoleSchema,
createdAt: z.coerce.date(),
updatedAt: z.coerce.date()
email: z.string().email(),
password: z.string(),
user_role: userRoleSchema,
shop_id: z.number().nullable(),
created_at: z.coerce.date(),
updated_at: z.coerce.date(),
last_login: z.coerce.date().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
phone_number: z.string().optional(),
profile_picture: z.string().url().optional(),
});
export type User = z.infer<typeof userSchema>;
export const userListSchema = z.array(userSchema);
+3 -3
View File
@@ -16,15 +16,15 @@ export const users = Array.from({ length: 20 }, () => {
"active",
"inactive",
"invited",
"suspended"
"suspended",
]),
role: faker.helpers.arrayElement([
"superadmin",
"admin",
"cashier",
"manager"
"manager",
]),
createdAt: faker.date.past(),
updatedAt: faker.date.recent()
updatedAt: faker.date.recent(),
};
});
+32 -9
View File
@@ -8,12 +8,23 @@ import { UsersDialogs } from "./components/users-dialogs";
import { UsersPrimaryButtons } from "./components/users-primary-buttons";
import { UsersTable } from "./components/users-table";
import UsersProvider from "./context/users-context";
import { userListSchema } from "./data/schema";
import { users } from "./data/users";
import { User, userListSchema } from "./data/schema";
import { useUsers } from "@/hooks/useUsers";
export default function Users() {
// Parse user list
const userList = userListSchema.parse(users);
const { data, error, isLoading } = useUsers();
let userList: User[] = [];
if (data) {
try {
userList = userListSchema.parse(data);
} catch (e) {
console.error("Schema validation failed:", e);
userList = [];
}
}
console.log(userList);
return (
<UsersProvider>
@@ -28,16 +39,28 @@ export default function Users() {
<Main>
<div className="mb-2 flex flex-wrap items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">User List</h2>
<h2 className="text-2xl font-bold tracking-tight">
Your customers
</h2>
<p className="text-muted-foreground">
Manage your users and their roles here.
Manage your customers user accounts here.
</p>
</div>
<UsersPrimaryButtons />
</div>
<div className="-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0">
<UsersTable data={userList} columns={columns} />
</div>
{isLoading && <p className="text-muted-foreground">Loading users...</p>}
{error && (
<p className="text-red-500">
Failed to load users: {(error as Error).message}
</p>
)}
{!isLoading && !error && (
<div className="-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0">
<UsersTable data={userList} columns={columns} />
</div>
)}
</Main>
<UsersDialogs />