Final commit (most likely)

This commit is contained in:
Thastertyn 2025-04-22 00:16:08 +02:00
parent f2af9dc566
commit 6903a8deb0
73 changed files with 1593 additions and 3492 deletions

View File

@ -1,25 +1,41 @@
# Base Image
FROM python:3.13
FROM python:3.12
RUN pip install poetry
# Environment variables
ENV PYTHONUNBUFFERED=1
# Set working directory
WORKDIR /app/
# Copy dependency files first to leverage caching
COPY pyproject.toml poetry.lock /app/
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
# Place executables in the environment at the front of the path
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
ENV PATH="/app/.venv/bin:$PATH"
# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1
# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy
# Install dependencies
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project
ENV PYTHONPATH=/app
COPY ./pyproject.toml ./uv.lock /app/
# Copy the rest of the application
COPY ./app /app/app
# Ensure dependencies are installed correctly
RUN poetry install --no-interaction --no-ansi --without dev
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync
# Expose port for FastAPI
EXPOSE 8000
# Command to run the app
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]

View File

@ -1,68 +1,55 @@
version: "3.9"
services:
backend:
build:
context: ./backend
container_name: swagshop-backend
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "8000:8000"
frontend:
build:
context: ./frontend/
container_name: swagshop-frontend
env_file:
- ./frontend/.env
ports:
- "5173:5173"
db:
image: postgres:12
restart: no
image: postgres:16
container_name: swagshop-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: swagshop
volumes:
- app-db-data:/var/lib/postgresql/data/pgdata
env_file:
- .env
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
adminer:
image: adminer
restart: no
- pgdata:/var/lib/postgresql/data
networks:
- default
depends_on:
- db
environment:
- ADMINER_DESIGN=pepa-linha-dark
backend:
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
restart: no
networks:
- default
depends_on:
db:
condition: service_healthy
env_file:
- .env
environment:
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
- ENVIRONMENT=${ENVIRONMENT}
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
- SECRET_KEY=${SECRET_KEY?Variable not set}
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
- SMTP_HOST=${SMTP_HOST}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
- MYSQL_SERVER=db
- MYSQL_PORT=${MYSQL_PORT}
- MYSQL_DB=${MYSQL_DB}
- MYSQL_USER=${MYSQL_USER?Variable not set}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
interval: 10s
timeout: 5s
retries: 5
build:
context: ./backend
- internal
volumes:
app-db-data:
pgdata:
networks:
internal:
driver: bridge

2
frontend/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -1 +1,2 @@
VITE_API_URL=
VITE_API_URL=http://localhost:8000
VITE_USE_MOCK_API=true

14
frontend/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:20
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build && npm install -g serve
EXPOSE 5173
CMD ["serve", "-s", "dist", "-l", "5173"]

View File

@ -0,0 +1,63 @@
import { MockPurchaseAPI } from "../purchase-mock-api";
import { mockDB } from "../db";
export async function createSamplePurchase() {
const users = await mockDB.users.where("user_role").equals("customer").toArray();
if (users.length === 0) throw new Error("No customers found");
const user = users[Math.floor(Math.random() * users.length)];
const products = await mockDB.products.toArray();
if (products.length === 0) throw new Error("No products found");
const entries = [];
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 4) + 1;
entries.push({
purchase_id: 0,
product_id: product.id,
product_variant_id: 0,
quantity,
});
}
if (entries.length === 0) throw new Error("No suitable product variants found");
const total = await entries.reduce(async (accP, entry) => {
const acc = await accP;
const product = await mockDB.products.get(entry.product_id);
return acc + ((product?.price ?? 0) * entry.quantity);
}, Promise.resolve(0));
// Randomly apply a valid coupon
const coupons = await mockDB.coupons.toArray();
const validCoupons = coupons.filter(
(c) => new Date(c.valid_due) > new Date()
);
const useCoupon = validCoupons.length > 0 && Math.random() < 0.5;
const chosenCoupon = useCoupon
? validCoupons[Math.floor(Math.random() * validCoupons.length)]
: null;
const discountedTotal = chosenCoupon
? Math.max(0, total - chosenCoupon.discount_amount)
: total;
const purchase = await MockPurchaseAPI.createPurchase(
{
user_id: user.id,
used_coupon_id: chosenCoupon?.id ?? null,
date_purchased: new Date().toISOString(),
total: discountedTotal,
},
entries,
);
console.log(
`Created mock purchase${chosenCoupon ? " with coupon" : ""}:`,
purchase,
);
}

View File

@ -31,7 +31,7 @@ class MockDB extends Dexie {
constructor() {
super("MockDB");
this.version(1).stores({
users: "++id,username,email,uuid",
users: "++id,username,email,uuid,user_role",
preferences: "user_id",
statistics: "user_id",
shops: "++id,uuid,name",

View File

@ -86,7 +86,7 @@ export interface ProductCreate {
description: string;
price: number;
stock_quantity: number;
image_data: string | undefined;
image_data?: string | undefined;
}
export interface ProductWithDetails extends Product {

View File

@ -55,10 +55,6 @@ export const MockProductAPI = {
.where("product_id")
.equals(productId)
.toArray();
const variants = await mockDB.product_variants
.where("product_id")
.equals(productId)
.toArray();
const categoryLinks = await mockDB.product_category_junctions
.where("product_id")
.equals(productId)
@ -76,7 +72,6 @@ export const MockProductAPI = {
return {
...product,
images,
variants,
categories,
};
},

View File

@ -53,7 +53,7 @@ export const MockPurchaseAPI = {
async createPurchase(
purchase: Omit<Purchase, "id">,
entries: Omit<PurchaseEntry, "id">[],
entries: Omit<PurchaseEntry, "id">[] = [],
): Promise<Purchase> {
const id = Date.now();
const newPurchase: Purchase = {
@ -63,6 +63,7 @@ export const MockPurchaseAPI = {
await mockDB.purchases.add(newPurchase);
if (entries.length > 0) {
await mockDB.purchase_entries.bulkAdd(
entries.map((entry) => ({
...entry,
@ -70,10 +71,23 @@ export const MockPurchaseAPI = {
id: Date.now() + Math.random(),
})),
);
}
return newPurchase;
},
async updatePurchase(
purchaseId: number,
updates: Partial<Omit<Purchase, "id">>,
): Promise<Purchase | null> {
const existing = await mockDB.purchases.get(purchaseId);
if (!existing) return null;
const updated = { ...existing, ...updates };
await mockDB.purchases.put(updated);
return updated;
},
async deletePurchase(purchaseId: number): Promise<boolean> {
await mockDB.purchase_entries
.where("purchase_id")

View File

@ -1,4 +1,4 @@
const currencies = [
export const currencies = [
"AED",
"AFN",
"ALL",

View File

@ -1,27 +1,14 @@
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarRail,
} from "@/components/ui/sidebar";
import { NavGroup } from "@/components/layout/nav-group";
import { sidebarData } from "./data/sidebar-data";
import { cn } from "@/lib/utils";
import { IconCoin } from "@tabler/icons-react";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" variant="floating" {...props}>
<SidebarHeader>
<h1
className={cn(
"header-fixed peer/header flex h-16 w-[inherit] items-center gap-3 rounded-md bg-background p-4 text-xl font-bold sm:gap-4",
)}
{...props}
>
<IconCoin />
</h1>
</SidebarHeader>
<SidebarContent>
{sidebarData.navGroups.map((props) => (
<NavGroup key={props.title} {...props} />

View File

@ -1,19 +1,13 @@
import {
IconBrowserCheck,
import
{
IconBuildingStore,
IconClipboardCheckFilled,
IconCoin,
IconForklift,
IconHelp,
IconLayoutDashboard,
IconNotification,
IconPackage,
IconCoin, IconPackage,
IconPalette,
IconSettings,
IconTag,
IconTool,
IconUserCog,
IconUsers,
IconUsers
} from "@tabler/icons-react";
import { type SidebarData } from "../types";
@ -22,11 +16,11 @@ export const sidebarData: SidebarData = {
{
title: "Dashboard",
items: [
{
title: "Dashboard",
url: "/dashboard",
icon: IconLayoutDashboard,
},
// {
// title: "Dashboard",
// url: "/dashboard",
// icon: IconLayoutDashboard,
// },
{
title: "Shop",
url: "/dashboard/shop",
@ -37,11 +31,6 @@ export const sidebarData: SidebarData = {
url: "/dashboard/products",
icon: IconPackage,
},
{
title: "Inventory",
url: "/dashboard/tasks",
icon: IconForklift,
},
{
title: "Sales",
icon: IconCoin,
@ -77,33 +66,14 @@ export const sidebarData: SidebarData = {
url: "/dashboard/settings",
icon: IconUserCog,
},
{
title: "Account",
url: "/dashboard/settings/account",
icon: IconTool,
},
{
title: "Appearance",
url: "/dashboard/settings/appearance",
icon: IconPalette,
},
{
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: IconNotification,
},
{
title: "Display",
url: "/dashboard/settings/display",
icon: IconBrowserCheck,
},
}
],
},
{
title: "Help Center",
url: "/help-center",
icon: IconHelp,
},
],
},
],

View File

@ -1,114 +0,0 @@
import { Link } from "@tanstack/react-router";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">SN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to="/settings/account">
<BadgeCheck />
Account
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings">
<CreditCard />
Billing
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/settings/notifications">
<Bell />
Notifications
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@ -18,9 +18,9 @@ const MainNavbar = () => {
<li className="font-medium text-primary">
<a href="#home">Home</a>
</li>
<li>
{/* <li>
<a href="#pricing">Pricing</a>
</li>
</li> */}
<li>
<a href="#faqs">FAQs</a>
</li>
@ -43,9 +43,9 @@ const MainNavbar = () => {
<DropdownMenuItem>
<a href="#features">Features</a>
</DropdownMenuItem>
<DropdownMenuItem>
{/* <DropdownMenuItem>
<a href="#pricing">Pricing</a>
</DropdownMenuItem>
</DropdownMenuItem> */}
<DropdownMenuItem>
<a href="#faqs">FAQs</a>
</DropdownMenuItem>

View File

@ -45,12 +45,6 @@ export function ProfileDropdown() {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to="/dashboard/settings">
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/dashboard/settings">
Settings

View File

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

View File

@ -1,9 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { PurchaseWithDetails } from "@/api/mock/models";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Purchase,
PurchaseWithDetails,
} from "@/api/mock/models";
import { purchaseAPI } from "@/api/api";
export function usePurchase(purchaseId?: number) {
return useQuery<PurchaseWithDetails | null>({
const queryClient = useQueryClient();
const purchase = useQuery<PurchaseWithDetails | null>({
queryKey: ["purchase", purchaseId],
queryFn: () => {
if (purchaseId === undefined) return Promise.resolve(null);
@ -11,4 +16,34 @@ export function usePurchase(purchaseId?: number) {
},
enabled: purchaseId !== undefined,
});
const createPurchase = useMutation({
mutationFn: (data: Omit<Purchase, "id">) => purchaseAPI.createPurchase(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["purchases"] });
},
});
const updatePurchase = useMutation({
mutationFn: (data: Purchase) => purchaseAPI.updatePurchase(data.id, data),
onSuccess: (_, updatedPurchase) => {
queryClient.invalidateQueries({ queryKey: ["purchases"] });
queryClient.invalidateQueries({ queryKey: ["purchase", updatedPurchase.id] });
},
});
const deletePurchase = useMutation({
mutationFn: (id: number) => purchaseAPI.deletePurchase(id),
onSuccess: (_, id) => {
queryClient.invalidateQueries({ queryKey: ["purchases"] });
queryClient.removeQueries({ queryKey: ["purchase", id] });
},
});
return {
purchase,
createPurchase,
updatePurchase,
deletePurchase,
};
}

View File

@ -1,7 +1,7 @@
import MainNavbar from "@/components/main-navbar";
import Features from "./components/features";
import Hero from "./components/hero";
import Pricing from "./components/pricing";
// import Pricing from "./components/pricing";
import Faq from "./components/faq";
import CallToAction from "./components/call-to-action";
@ -14,7 +14,7 @@ export default function MainPage() {
<div className="h-[20vh] min-h-[20vh]"></div>
<Hero />
<Features />
<Pricing />
{/* <Pricing /> */}
<Faq />
<CallToAction />
</div>

View File

@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
import { UseFormReturn } from "react-hook-form";
import { z } from "zod";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const schema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().min(1, "Description is required"),
@ -18,12 +19,11 @@ type ProductForm = z.infer<typeof schema>;
export function ProductForm({
onSubmit,
form,
imagePreview,
onImageUpload,
submitLabel,
}: {
onSubmit: () => void;
form: UseFormReturn<typeof ProductForm>;
form: UseFormReturn<ProductForm>;
imagePreview?: string;
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
submitLabel: string;

View File

@ -96,6 +96,7 @@ export const columns: ColumnDef<Coupon>[] = [
);
},
filterFn: (row, id, value) => {
console.log(id);
const validDue = new Date(row.original.valid_due);
const now = new Date();
const status = isBefore(validDue, now) ? "expired" : "active";

View File

@ -27,9 +27,9 @@ export function DataTableToolbar<TData>({
className="h-8 w-[150px] lg:w-[250px]"
/>
<div className="flex gap-x-2">
{table.getColumn("valid_due") && (
{table.getColumn("Valid Due") && (
<DataTableFacetedFilter
column={table.getColumn("valid_due")}
column={table.getColumn("Valid Due")}
title="Status"
options={couponTypes.map((t) => ({ ...t }))}
/>

View File

@ -24,8 +24,6 @@ export default function Coupons() {
}
}
console.log(couponList);
return (
<CouponsProvider>
<Header fixed>

View File

@ -37,12 +37,11 @@ export function DataTableFacetedFilter<TData, TValue>({
}: 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="mr-2 h-4 w-4" />
<PlusCircledIcon className="h-4 w-4" />
{title}
{selectedValues?.size > 0 && (
<>
@ -104,7 +103,7 @@ export function DataTableFacetedFilter<TData, TValue>({
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
"flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
@ -113,7 +112,7 @@ export function DataTableFacetedFilter<TData, TValue>({
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
<option.icon className="h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (

View File

@ -0,0 +1,45 @@
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Row } from "@tanstack/react-table";
import { IconEdit } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import { usePurchasesContext } from "../context/purchase-context";
import { Purchase } from "../data/schema";
interface DataTableRowActionsProps {
row: Row<Purchase>;
}
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
const { setOpen, setCurrentRow } = usePurchasesContext();
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("view");
}}>
View
<DropdownMenuShortcut>
<IconEdit size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -2,9 +2,9 @@ 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 { DataTableViewOptions } from "../components/data-table-view-options";
import { priorities, statuses } from "../data/data";
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>;
@ -19,26 +19,19 @@ export function DataTableToolbar<TData>({
<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 tasks..."
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
placeholder="Filter coupons..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("title")?.setFilterValue(event.target.value)
table.getColumn("name")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
<div className="flex gap-x-2">
{table.getColumn("status") && (
{table.getColumn("valid_due") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
column={table.getColumn("valid_due")}
title="Status"
options={statuses}
/>
)}
{table.getColumn("priority") && (
<DataTableFacetedFilter
column={table.getColumn("priority")}
title="Priority"
options={priorities}
options={couponTypes.map((t) => ({ ...t }))}
/>
)}
</div>

View File

@ -0,0 +1,219 @@
"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 { Purchase } from "../data/schema";
import { usePurchase } from "@/hooks/usePurchase";
const formSchema = z.object({
user_id: z.number({ invalid_type_error: "User ID must be a number." }),
used_coupon_id: z.number().nullable().optional(),
total: z
.number({ invalid_type_error: "Total must be a number." })
.min(0, { message: "Total must be at least 0." }),
date_purchased: z.coerce.date({
errorMap: () => ({ message: "Invalid date." })
}),
isEdit: z.boolean()
});
type PurchaseForm = z.infer<typeof formSchema>;
interface Props {
currentRow?: Purchase;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function PurchasesActionDialog({
currentRow,
open,
onOpenChange
}: Props) {
const { createPurchase, updatePurchase } = usePurchase(currentRow?.id);
const isEdit = !!currentRow;
const form = useForm<PurchaseForm>({
resolver: zodResolver(formSchema),
defaultValues: isEdit
? {
...currentRow,
date_purchased: new Date(currentRow.date_purchased),
isEdit
}
: {
user_id: 0,
used_coupon_id: null,
total: 0,
date_purchased: new Date(),
isEdit
}
});
const onSubmit = (values: PurchaseForm) => {
try {
const payload = {
...values,
date_purchased: values.date_purchased.toISOString()
};
if (isEdit) {
updatePurchase.mutate({ ...currentRow, ...payload });
} else {
createPurchase.mutate({...payload, used_coupon_id: payload.used_coupon_id ?? null });
}
toast({
title: isEdit ? "Purchase updated" : "Purchase created",
description: `User #${values.user_id}${values.total.toFixed(2)}`
});
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 Purchase" : "Add New Purchase"}
</DialogTitle>
<DialogDescription>
{isEdit
? "Update the purchase details below."
: "Record a new purchase."}
</DialogDescription>
</DialogHeader>
<ScrollArea className="-mr-4 h-[26.25rem] w-full py-1 pr-4">
<Form {...form}>
<form
id="purchase-form"
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4 p-0.5">
<FormField
control={form.control}
name="user_id"
render={({ field }) => (
<FormItem>
<FormLabel>User ID</FormLabel>
<FormControl>
<Input
type="number"
placeholder="e.g., 12"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="used_coupon_id"
render={({ field }) => (
<FormItem>
<FormLabel>Coupon ID (optional)</FormLabel>
<FormControl>
<Input
type="number"
placeholder="e.g., 5"
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
field.onChange(value === "" ? null : Number(value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="total"
render={({ field }) => (
<FormItem>
<FormLabel>Total ()</FormLabel>
<FormControl>
<Input
type="number"
step="0.01"
placeholder="e.g., 12.50"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="date_purchased"
render={({ field }) => (
<FormItem>
<FormLabel>Purchase Date</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="purchase-form">
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,95 @@
import { ColumnDef } from "@tanstack/react-table";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils";
import { Purchase } from "../data/schema";
import { DataTableColumnHeader } from "./data-table-column-header";
import { DataTableRowActions } from "./data-table-row-actions";
import { format } from "date-fns";
import { Badge } from "@/components/ui/badge";
import { Coupon } from "@/api/mock/models";
export function getColumns(
couponsMap: Map<number, Coupon>
): ColumnDef<Purchase>[] {
return [
{
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: "id",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Purchase ID" />
),
cell: ({ row }) => (
<div className="font-medium">{row.getValue("id")}</div>
),
enableHiding: false
},
{
accessorKey: "total",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Total" />
),
cell: ({ row }) => {
const value = row.getValue("total") as number;
return <div className="font-medium">{value.toFixed(2)}</div>;
}
},
{
accessorKey: "date_purchased",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Date" />
),
cell: ({ row }) => {
const date = new Date(row.getValue("date_purchased"));
return <div>{format(date, "PPPp")}</div>;
}
},
{
accessorKey: "used_coupon_id",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Coupon" />
),
cell: ({ row }) => {
const { used_coupon_id } = row.original;
const coupon = used_coupon_id ? couponsMap.get(used_coupon_id) : null;
return coupon ? (
<Badge variant="outline">{coupon.name}</Badge>
) : (
<span className="text-muted-foreground"></span>
);
}
},
{
id: "actions",
cell: DataTableRowActions
}
];
}

View File

@ -0,0 +1,65 @@
"use client";
import { toast } from "@/hooks/useToast";
import { IconAlertTriangle } from "@tabler/icons-react";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { Purchase } from "../data/schema";
import { usePurchase } from "@/hooks/usePurchase";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
currentRow: Purchase;
}
export function PurchasesDeleteDialog({ open, onOpenChange, currentRow }: Props) {
const { deletePurchase } = usePurchase(currentRow.id);
const handleDelete = () => {
deletePurchase.mutate(currentRow.id);
onOpenChange(false);
toast({
title: "Purchase deleted",
description: `Purchase #${currentRow.id} has been successfully 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 Purchase
</span>
}
desc={
<div className="space-y-4">
<p>
Are you sure you want to delete{" "}
<span className="font-bold">purchase #{currentRow.id}</span>?
<br />
This action cannot be undone.
</p>
<Alert variant="destructive">
<AlertTitle>Warning!</AlertTitle>
<AlertDescription>
This will permanently remove this purchase and all associated
entries.
</AlertDescription>
</Alert>
</div>
}
confirmText="Delete"
destructive
/>
);
}

View File

@ -0,0 +1,45 @@
import { usePurchasesContext } from "../context/purchase-context";
import { PurchasesActionDialog } from "./purchase-action-dialog";
import { PurchasesDeleteDialog } from "./purchase-delete-dialog";
export function PurchasesDialogs() {
const { open, setOpen, currentRow, setCurrentRow } = usePurchasesContext();
return (
<>
<PurchasesActionDialog
key="purchase-add"
open={open === "add"}
onOpenChange={() => setOpen("add")}
/>
{currentRow && (
<>
<PurchasesActionDialog
key={`purchase-edit-${currentRow.id}`}
open={open === "view"}
onOpenChange={() => {
setOpen("view");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
currentRow={currentRow}
/>
<PurchasesDeleteDialog
key={`purchase-delete-${currentRow.id}`}
open={open === "delete"}
onOpenChange={() => {
setOpen("delete");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
currentRow={currentRow}
/>
</>
)}
</>
);
}

View File

@ -0,0 +1,14 @@
import { IconPlus } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { usePurchasesContext } from "../context/purchase-context";
export function PurchasePrimaryButtons() {
const { setOpen } = usePurchasesContext();
return (
<div className="flex gap-2">
<Button className="space-x-1" onClick={() => setOpen("add")}>
<span>Add New Purchase</span> <IconPlus size={18} />
</Button>
</div>
);
}

View File

@ -1,7 +1,8 @@
import * as React from "react";
import { useState } from "react";
import {
ColumnDef,
ColumnFiltersState,
RowData,
SortingState,
VisibilityState,
flexRender,
@ -21,25 +22,27 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { DataTablePagination } from "../components/data-table-pagination";
import { DataTableToolbar } from "../components/data-table-toolbar";
import { Purchase } from "../data/schema";
import { DataTablePagination } from "./data-table-pagination";
import { DataTableToolbar } from "./data-table-toolbar";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
className: string;
}
}
export function DataTable<TData, TValue>({
columns,
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>([]);
interface DataTableProps {
columns: ColumnDef<Purchase>[];
data: Purchase[];
}
export function PurchasesTable({ 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,
@ -70,10 +73,13 @@ export function DataTable<TData, TValue>({
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
<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(
@ -81,20 +87,23 @@ export function DataTable<TData, TValue>({
header.getContext(),
)}
</TableHead>
);
})}
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
{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}>
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.className ?? ""}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import useDialogState from "@/hooks/useDialogState";
import { Purchase } from "../data/schema";
type PurchasesDialogType = "view" | "delete" | "edit" | "add";
interface PurchasesContextType {
open: PurchasesDialogType | null;
setOpen: (type: PurchasesDialogType | null) => void;
currentRow: Purchase | null;
setCurrentRow: React.Dispatch<React.SetStateAction<Purchase | null>>;
}
const PurchasesContext = React.createContext<PurchasesContextType | null>(null);
interface Props {
children: React.ReactNode;
}
export default function PurchasesProvider({ children }: Props) {
const [open, setOpen] = useDialogState<PurchasesDialogType>(null);
const [currentRow, setCurrentRow] = useState<Purchase | null>(null);
return (
<PurchasesContext.Provider
value={{ open, setOpen, currentRow, setCurrentRow }}
>
{children}
</PurchasesContext.Provider>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const usePurchasesContext = () => {
const context = React.useContext(PurchasesContext);
if (!context) {
throw new Error("usePurchasesContext must be used within <PurchasesProvider>");
}
return context;
};

View File

@ -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;

View File

@ -0,0 +1,13 @@
import { z } from "zod";
export const purchaseSchema = z.object({
id: z.number(),
user_id: z.number(),
used_coupon_id: z.number().nullable(),
date_purchased: z.string(),
total: z.number().nonnegative(),
});
export type Purchase = z.infer<typeof purchaseSchema>;
export const purchaseListSchema = z.array(purchaseSchema);

View File

@ -0,0 +1,75 @@
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 { getColumns } from "./components/purchase-columns";
import { PurchasesDialogs } from "./components/purchase-dialog";
import { PurchasePrimaryButtons } from "./components/purchase-primary-buttons";
import { PurchasesTable } from "./components/purchase-table";
import PurchasesProvider from "./context/purchase-context";
import { usePurchases } from "@/hooks/usePurchases";
import { Coupon, Purchase } from "@/api/mock/models";
import { purchaseListSchema } from "./data/schema";
import { useCoupons } from "@/hooks/useCoupons";
export default function RecentSalves() {
const { data, error, isLoading } = usePurchases();
const { data: couponsData } = useCoupons();
const couponsMap = new Map<number, Coupon>();
couponsData?.forEach((c) => couponsMap.set(c.id, c));
let purchaseList: Purchase[] = [];
if (data) {
try {
purchaseList = purchaseListSchema.parse(data);
} catch (e) {
console.error("Schema validation failed:", e);
purchaseList = [];
}
}
return (
<PurchasesProvider>
<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">Purchases</h2>
<p className="text-muted-foreground">
View, and analyze user purchases.
</p>
</div>
<PurchasePrimaryButtons />
</div>
{isLoading && (
<p className="text-muted-foreground">Loading purchases...</p>
)}
{error && (
<p className="text-red-500">
Failed to load purchases: {(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">
<PurchasesTable
data={purchaseList}
columns={getColumns(couponsMap)}
/>
</div>
)}
</Main>
<PurchasesDialogs />
</PurchasesProvider>
);
}

View File

@ -1,135 +0,0 @@
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 { Input } from "@/components/ui/input";
import useAuth from "@/hooks/useAuth";
// const languages = [
// { label: "English", value: "en" },
// { label: "French", value: "fr" },
// { label: "German", value: "de" },
// { label: "Spanish", value: "es" },
// { label: "Portuguese", value: "pt" },
// { label: "Russian", value: "ru" },
// { label: "Japanese", value: "ja" },
// { label: "Korean", value: "ko" },
// { label: "Chinese", value: "zh" }
// ] as const;
const accountFormSchema = z.object({
username: z.string().min(3).max(30),
email: z.string().email(),
phone_number: z.string(),
// password: z.string().min(8).max(128),
first_name: z.string().optional(),
last_name: z.string().optional(),
// dob: z.date({
// required_error: "A date of birth is required."
// }),
// language: z.string({
// required_error: "Please select a language."
// })
});
type AccountFormValues = z.infer<typeof accountFormSchema>;
export function AccountForm() {
const { updateAccountMutation, user } = useAuth();
const form = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
});
function onSubmit(data: AccountFormValues) {
updateAccountMutation.mutate({
email: data.email,
// password: data.password,
password: "null",
phone_number: data.phone_number,
username: data.username,
first_name: data.first_name,
last_name: data.last_name,
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Your username"
defaultValue={user?.username}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="Your email"
defaultValue={user?.email}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input
placeholder="Your phone number"
defaultValue={user?.phone_number}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="New password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Account</Button>
</form>
</Form>
);
}

View File

@ -1,14 +0,0 @@
import ContentSection from "../components/content-section";
import { AccountForm } from "./account-form";
export default function SettingsAccount() {
return (
<ContentSection
title="Account"
desc="Update your account settings. Set your preferred language and
timezone."
>
<AccountForm />
</ContentSection>
);
}

View File

@ -1,129 +0,0 @@
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 { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
const items = [
{
id: "recents",
label: "Recents",
},
{
id: "home",
label: "Home",
},
{
id: "applications",
label: "Applications",
},
{
id: "desktop",
label: "Desktop",
},
{
id: "downloads",
label: "Downloads",
},
{
id: "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.",
}),
});
type DisplayFormValues = z.infer<typeof displayFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<DisplayFormValues> = {
items: ["recents", "home"],
};
export function DisplayForm() {
const form = useForm<DisplayFormValues>({
resolver: zodResolver(displayFormSchema),
defaultValues,
});
function onSubmit(data: DisplayFormValues) {
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(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="items"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel className="text-base">Sidebar</FormLabel>
<FormDescription>
Select the items you want to display in the sidebar.
</FormDescription>
</div>
{items.map((item) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id,
),
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.label}
</FormLabel>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update display</Button>
</form>
</Form>
);
}

View File

@ -1,13 +0,0 @@
import ContentSection from "../components/content-section";
import { DisplayForm } from "./display-form";
export default function SettingsDisplay() {
return (
<ContentSection
title="Display"
desc="Turn items on or off to control what's displayed in the app."
>
<DisplayForm />
</ContentSection>
);
}

View File

@ -1,10 +1,8 @@
import { Outlet } from "@tanstack/react-router";
import {
IconBrowserCheck,
IconLock,
IconNotification,
IconPalette,
IconUser,
import
{
IconLock, IconPalette,
IconUser
} from "@tabler/icons-react";
import { Separator } from "@/components/ui/separator";
import { Header } from "@/components/layout/header";
@ -64,15 +62,5 @@ const sidebarNavItems = [
title: "Appearance",
icon: <IconPalette size={18} />,
href: "/dashboard/settings/appearance",
},
{
title: "Notifications",
icon: <IconNotification size={18} />,
href: "/dashboard/settings/notifications",
},
{
title: "Display",
icon: <IconBrowserCheck size={18} />,
href: "/dashboard/settings/display",
},
}
];

View File

@ -1,13 +0,0 @@
import ContentSection from "../components/content-section";
import { NotificationsForm } from "./notifications-form";
export default function SettingsNotifications() {
return (
<ContentSection
title="Notifications"
desc="Configure how you receive notifications."
>
<NotificationsForm />
</ContentSection>
);
}

View File

@ -1,225 +0,0 @@
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Link } from "@tanstack/react-router";
import { toast } from "@/hooks/useToast";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
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.",
}),
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(),
});
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<NotificationsFormValues> = {
communication_emails: false,
marketing_emails: false,
social_emails: true,
security_emails: true,
};
export function NotificationsForm() {
const form = useForm<NotificationsFormValues>({
resolver: zodResolver(notificationsFormSchema),
defaultValues,
});
function onSubmit(data: NotificationsFormValues) {
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(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="relative space-y-3">
<FormLabel>Notify me about...</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="all" />
</FormControl>
<FormLabel className="font-normal">
All new messages
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="mentions" />
</FormControl>
<FormLabel className="font-normal">
Direct messages and mentions
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="none" />
</FormControl>
<FormLabel className="font-normal">Nothing</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="relative">
<h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
<div className="space-y-4">
<FormField
control={form.control}
name="communication_emails"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Communication emails
</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="marketing_emails"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products, features, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="social_emails"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Social emails</FormLabel>
<FormDescription>
Receive emails for friend requests, follows, and more.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="security_emails"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">Security emails</FormLabel>
<FormDescription>
Receive emails about your account activity and security.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled
aria-readonly
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
<FormField
control={form.control}
name="mobile"
render={({ field }) => (
<FormItem className="relative flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Use different settings for my mobile devices
</FormLabel>
<FormDescription>
You can manage your mobile notifications in the{" "}
<Link
to="/settings"
className="underline decoration-dashed underline-offset-4 hover:decoration-solid"
>
mobile settings
</Link>{" "}
page.
</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit">Update notifications</Button>
</form>
</Form>
);
}

View File

@ -1,10 +0,0 @@
import ContentSection from "../components/content-section";
import PasswordChangeForm from "./security-form";
export default function SettingsSecurity() {
return (
<ContentSection title="Security" desc="You can change your password here">
<PasswordChangeForm />
</ContentSection>
);
}

View File

@ -1,116 +0,0 @@
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 { PasswordInput } from "@/components/password-input";
import { toast } from "@/hooks/useToast";
const passwordChangeSchema = z
.object({
oldPassword: z.string().min(1, {
message: "Please enter your existing password",
}),
newPassword: z
.string()
.min(1, { message: "Please enter your new password" })
.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",
})
.refine((password) => /[a-z]/.test(password), {
message: "Password must contain at least one lowercase letter",
})
.refine((password) => /\d/.test(password), {
message: "Password must contain at least one number",
})
.refine((password) => /[@$!%*?&]/.test(password), {
message:
"Password must contain at least one special character (@, $, !, %, *, ?, &)",
}),
confirmNewPassword: z.string().min(1, {
message: "Please confirm your new password",
}),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: "Passwords don't match.",
path: ["confirmNewPassword"],
});
type PasswordChangeFormValues = z.infer<typeof passwordChangeSchema>;
export default function PasswordChangeForm() {
const form = useForm<PasswordChangeFormValues>({
resolver: zodResolver(passwordChangeSchema),
defaultValues: {
oldPassword: "",
newPassword: "",
confirmNewPassword: "",
},
});
function onSubmit(data: PasswordChangeFormValues) {
toast({
title: "Password Changed (MOCK)",
description: "Your password has been updated.",
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Existing Password</FormLabel>
<FormControl>
<PasswordInput
placeholder="Enter current password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<PasswordInput placeholder="Enter new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmNewPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<PasswordInput placeholder="Confirm new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Change Password</Button>
</form>
</Form>
);
}

View File

@ -17,6 +17,7 @@ import { Badge } from "@/components/ui/badge";
import { IconBuildingStore, IconClock, IconLink } from "@tabler/icons-react";
export default function ShopSidebar({ ...props }) {
console.log(props);
const { pathname } = useLocation();
const navigate = useNavigate();
const [val, setVal] = useState(pathname ?? "/settings");

View File

@ -4,7 +4,7 @@ import { ProfileDropdown } from "@/components/profile-dropdown";
import { ThemeSwitch } from "@/components/theme-switch";
import { ShopAboutForm } from "./components/shop-about-form";
import { Search } from "@/components/search";
import ShopSidebar from "./components/shop-sidebar";
// import ShopSidebar from "./components/shop-sidebar";
import { Separator } from "@/components/ui/separator";
export default function AboutPage() {
@ -30,7 +30,7 @@ export default function AboutPage() {
</div>
<Separator />
<div className="my-4 flex flex-row gap-4">
<ShopSidebar />
{/* <ShopSidebar /> */}
<ShopAboutForm />
</div>

View File

@ -1,119 +0,0 @@
import { ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { labels, priorities, statuses } from "../data/data";
import { Task } from "../data/schema";
import { DataTableColumnHeader } from "./data-table-column-header";
import { DataTableRowActions } from "./data-table-row-actions";
export const columns: ColumnDef<Task>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
className="translate-y-[2px]"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="translate-y-[2px]"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "id",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Task" />
),
cell: ({ row }) => <div className="w-[80px]">{row.getValue("id")}</div>,
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "title",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Title" />
),
cell: ({ row }) => {
const label = labels.find((label) => label.value === row.original.label);
return (
<div className="flex space-x-2">
{label && <Badge variant="outline">{label.label}</Badge>}
<span className="max-w-32 truncate font-medium sm:max-w-72 md:max-w-[31rem]">
{row.getValue("title")}
</span>
</div>
);
},
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const status = statuses.find(
(status) => status.value === row.getValue("status"),
);
if (!status) {
return null;
}
return (
<div className="flex w-[100px] items-center">
{status.icon && (
<status.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{status.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
accessorKey: "priority",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Priority" />
),
cell: ({ row }) => {
const priority = priorities.find(
(priority) => priority.value === row.getValue("priority"),
);
if (!priority) {
return null;
}
return (
<div className="flex items-center">
{priority.icon && (
<priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{priority.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: ({ row }) => <DataTableRowActions row={row} />,
},
];

View File

@ -1,83 +0,0 @@
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Row } from "@tanstack/react-table";
import { IconTrash } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTasks } from "../context/tasks-context";
import { labels } from "../data/data";
import { taskSchema } from "../data/schema";
interface DataTableRowActionsProps<TData> {
row: Row<TData>;
}
export function DataTableRowActions<TData>({
row,
}: DataTableRowActionsProps<TData>) {
const task = taskSchema.parse(row.original);
const { setOpen, setCurrentRow } = useTasks();
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(task);
setOpen("update");
}}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem disabled>Make a copy</DropdownMenuItem>
<DropdownMenuItem disabled>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={task.label}>
{labels.map((label) => (
<DropdownMenuRadioItem key={label.value} value={label.value}>
{label.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
setCurrentRow(task);
setOpen("delete");
}}
>
Delete
<DropdownMenuShortcut>
<IconTrash size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,78 +0,0 @@
import { toast } from "@/hooks/useToast";
import { ConfirmDialog } from "@/components/confirm-dialog";
import { useTasks } from "../context/tasks-context";
import { TasksImportDialog } from "./tasks-import-dialog";
import { TasksMutateDrawer } from "./tasks-mutate-drawer";
export function TasksDialogs() {
const { open, setOpen, currentRow, setCurrentRow } = useTasks();
return (
<>
<TasksMutateDrawer
key="task-create"
open={open === "create"}
onOpenChange={() => setOpen("create")}
/>
<TasksImportDialog
key="tasks-import"
open={open === "import"}
onOpenChange={() => setOpen("import")}
/>
{currentRow && (
<>
<TasksMutateDrawer
key={`task-update-${currentRow.id}`}
open={open === "update"}
onOpenChange={() => {
setOpen("update");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
currentRow={currentRow}
/>
<ConfirmDialog
key="task-delete"
destructive
open={open === "delete"}
onOpenChange={() => {
setOpen("delete");
setTimeout(() => {
setCurrentRow(null);
}, 500);
}}
handleConfirm={() => {
setOpen(null);
setTimeout(() => {
setCurrentRow(null);
}, 500);
toast({
title: "The following task has been deleted:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">
{JSON.stringify(currentRow, null, 2)}
</code>
</pre>
),
});
}}
className="max-w-md"
title={`Delete this task: ${currentRow.id} ?`}
desc={
<>
You are about to delete a task with the ID{" "}
<strong>{currentRow.id}</strong>. <br />
This action cannot be undone.
</>
}
confirmText="Delete"
/>
</>
)}
</>
);
}

View File

@ -1,116 +0,0 @@
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,
DialogClose,
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";
const formSchema = z.object({
file: z
.instanceof(FileList)
.refine((files) => files.length > 0, {
message: "Please upload a file",
})
.refine(
(files) => ["text/csv"].includes(files?.[0]?.type),
"Please upload csv format.",
),
});
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function TasksImportDialog({ open, onOpenChange }: Props) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { file: undefined },
});
const fileRef = form.register("file");
const onSubmit = () => {
const file = form.getValues("file");
if (file && file[0]) {
const fileDetails = {
name: file[0].name,
size: file[0].size,
type: file[0].type,
};
toast({
title: "You have imported the following file:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">
{JSON.stringify(fileDetails, null, 2)}
</code>
</pre>
),
});
}
onOpenChange(false);
};
return (
<Dialog
open={open}
onOpenChange={(val) => {
onOpenChange(val);
form.reset();
}}
>
<DialogContent className="gap-2 sm:max-w-sm">
<DialogHeader className="text-left">
<DialogTitle>Import Tasks</DialogTitle>
<DialogDescription>
Import tasks quickly from a CSV file.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form id="task-import-form" onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="file"
render={() => (
<FormItem className="mb-2 space-y-1">
<FormLabel>File</FormLabel>
<FormControl>
<Input type="file" {...fileRef} className="h-8" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<DialogFooter className="gap-2 sm:gap-0">
<DialogClose asChild>
<Button variant="outline">Close</Button>
</DialogClose>
<Button type="submit" form="task-import-form">
Import
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,215 +0,0 @@
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 {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { SelectDropdown } from "@/components/select-dropdown";
import { Task } from "../data/schema";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
currentRow?: Task;
}
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."),
});
type TasksForm = z.infer<typeof formSchema>;
export function TasksMutateDrawer({ open, onOpenChange, currentRow }: Props) {
const isUpdate = !!currentRow;
const form = useForm<TasksForm>({
resolver: zodResolver(formSchema),
defaultValues: currentRow ?? {
title: "",
status: "",
label: "",
priority: "",
},
});
const onSubmit = (data: TasksForm) => {
// do something with the form data
onOpenChange(false);
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(data, null, 2)}</code>
</pre>
),
});
};
return (
<Sheet
open={open}
onOpenChange={(v) => {
onOpenChange(v);
form.reset();
}}
>
<SheetContent className="flex flex-col">
<SheetHeader className="text-left">
<SheetTitle>{isUpdate ? "Update" : "Create"} Task</SheetTitle>
<SheetDescription>
{isUpdate
? "Update the task by providing necessary info."
: "Add a new task by providing necessary info."}
Click save when you&apos;re done.
</SheetDescription>
</SheetHeader>
<Form {...form}>
<form
id="tasks-form"
onSubmit={form.handleSubmit(onSubmit)}
className="flex-1 space-y-5"
>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter a title" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Status</FormLabel>
<SelectDropdown
defaultValue={field.value}
onValueChange={field.onChange}
placeholder="Select dropdown"
items={[
{ label: "In Progress", value: "in progress" },
{ label: "Backlog", value: "backlog" },
{ label: "Todo", value: "todo" },
{ label: "Canceled", value: "canceled" },
{ label: "Done", value: "done" },
]}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="label"
render={({ field }) => (
<FormItem className="relative space-y-3">
<FormLabel>Label</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="documentation" />
</FormControl>
<FormLabel className="font-normal">
Documentation
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="feature" />
</FormControl>
<FormLabel className="font-normal">Feature</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="bug" />
</FormControl>
<FormLabel className="font-normal">Bug</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="relative space-y-3">
<FormLabel>Priority</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="high" />
</FormControl>
<FormLabel className="font-normal">High</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="medium" />
</FormControl>
<FormLabel className="font-normal">Medium</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="low" />
</FormControl>
<FormLabel className="font-normal">Low</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<SheetFooter className="gap-2">
<SheetClose asChild>
<Button variant="outline">Close</Button>
</SheetClose>
<Button form="tasks-form" type="submit">
Save changes
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@ -1,21 +0,0 @@
import { IconDownload, IconPlus } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { useTasks } from "../context/tasks-context";
export function TasksPrimaryButtons() {
const { setOpen } = useTasks();
return (
<div className="flex gap-2">
<Button
variant="outline"
className="space-x-1"
onClick={() => setOpen("import")}
>
<span>Import</span> <IconDownload size={18} />
</Button>
<Button className="space-x-1" onClick={() => setOpen("create")}>
<span>Create</span> <IconPlus size={18} />
</Button>
</div>
);
}

View File

@ -1,39 +0,0 @@
import React, { useState } from "react";
import useDialogState from "@/hooks/useDialogState";
import { Task } from "../data/schema";
type TasksDialogType = "create" | "update" | "delete" | "import";
interface TasksContextType {
open: TasksDialogType | null;
setOpen: (str: TasksDialogType | null) => void;
currentRow: Task | null;
setCurrentRow: React.Dispatch<React.SetStateAction<Task | null>>;
}
const TasksContext = React.createContext<TasksContextType | null>(null);
interface Props {
children: React.ReactNode;
}
export default function TasksProvider({ children }: Props) {
const [open, setOpen] = useDialogState<TasksDialogType>(null);
const [currentRow, setCurrentRow] = useState<Task | null>(null);
return (
<TasksContext value={{ open, setOpen, currentRow, setCurrentRow }}>
{children}
</TasksContext>
);
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTasks = () => {
const tasksContext = React.useContext(TasksContext);
if (!tasksContext) {
throw new Error("useTasks has to be used within <TasksContext>");
}
return tasksContext;
};

View File

@ -1,71 +0,0 @@
import {
IconArrowDown,
IconArrowRight,
IconArrowUp,
IconCircle,
IconCircleCheck,
IconCircleX,
IconExclamationCircle,
IconStopwatch,
} from "@tabler/icons-react";
export const labels = [
{
value: "bug",
label: "Bug",
},
{
value: "feature",
label: "Feature",
},
{
value: "documentation",
label: "Documentation",
},
];
export const statuses = [
{
value: "backlog",
label: "Backlog",
icon: IconExclamationCircle,
},
{
value: "todo",
label: "Todo",
icon: IconCircle,
},
{
value: "in progress",
label: "In Progress",
icon: IconStopwatch,
},
{
value: "done",
label: "Done",
icon: IconCircleCheck,
},
{
value: "canceled",
label: "Canceled",
icon: IconCircleX,
},
];
export const priorities = [
{
label: "Low",
value: "low",
icon: IconArrowDown,
},
{
label: "Medium",
value: "medium",
icon: IconArrowRight,
},
{
label: "High",
value: "high",
icon: IconArrowUp,
},
];

View File

@ -1,13 +0,0 @@
import { z } from "zod";
// We're keeping a simple non-relational schema here.
// IRL, you will have a schema for your data models.
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string(),
});
export type Task = z.infer<typeof taskSchema>;

View File

@ -1,782 +0,0 @@
export const tasks = [
{
id: "TASK-8782",
title:
"You can't compress the program without quantifying the open-source SSD pixel!",
status: "in progress",
label: "documentation",
priority: "medium",
},
{
id: "TASK-7878",
title:
"Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
status: "backlog",
label: "documentation",
priority: "medium",
},
{
id: "TASK-7839",
title: "We need to bypass the neural TCP card!",
status: "todo",
label: "bug",
priority: "high",
},
{
id: "TASK-5562",
title:
"The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!",
status: "backlog",
label: "feature",
priority: "medium",
},
{
id: "TASK-8686",
title:
"I'll parse the wireless SSL protocol, that should driver the API panel!",
status: "canceled",
label: "feature",
priority: "medium",
},
{
id: "TASK-1280",
title:
"Use the digital TLS panel, then you can transmit the haptic system!",
status: "done",
label: "bug",
priority: "high",
},
{
id: "TASK-7262",
title:
"The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!",
status: "done",
label: "feature",
priority: "high",
},
{
id: "TASK-1138",
title:
"Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
status: "in progress",
label: "feature",
priority: "medium",
},
{
id: "TASK-7184",
title: "We need to program the back-end THX pixel!",
status: "todo",
label: "feature",
priority: "low",
},
{
id: "TASK-5160",
title:
"Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
status: "in progress",
label: "documentation",
priority: "high",
},
{
id: "TASK-5618",
title:
"Generating the driver won't do anything, we need to index the online SSL application!",
status: "done",
label: "documentation",
priority: "medium",
},
{
id: "TASK-6699",
title:
"I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
status: "backlog",
label: "documentation",
priority: "medium",
},
{
id: "TASK-2858",
title: "We need to override the online UDP bus!",
status: "backlog",
label: "bug",
priority: "medium",
},
{
id: "TASK-9864",
title:
"I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
status: "done",
label: "bug",
priority: "high",
},
{
id: "TASK-8404",
title: "We need to generate the virtual HEX alarm!",
status: "in progress",
label: "bug",
priority: "low",
},
{
id: "TASK-5365",
title:
"Backing up the pixel won't do anything, we need to transmit the primary IB array!",
status: "in progress",
label: "documentation",
priority: "low",
},
{
id: "TASK-1780",
title:
"The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!",
status: "todo",
label: "documentation",
priority: "high",
},
{
id: "TASK-6938",
title:
"Use the redundant SCSI application, then you can hack the optical alarm!",
status: "todo",
label: "documentation",
priority: "high",
},
{
id: "TASK-9885",
title: "We need to compress the auxiliary VGA driver!",
status: "backlog",
label: "bug",
priority: "high",
},
{
id: "TASK-3216",
title:
"Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
status: "backlog",
label: "documentation",
priority: "medium",
},
{
id: "TASK-9285",
title:
"The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!",
status: "todo",
label: "bug",
priority: "high",
},
{
id: "TASK-1024",
title:
"Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
status: "in progress",
label: "documentation",
priority: "low",
},
{
id: "TASK-7068",
title:
"You can't generate the capacitor without indexing the wireless HEX pixel!",
status: "canceled",
label: "bug",
priority: "low",
},
{
id: "TASK-6502",
title:
"Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
status: "todo",
label: "bug",
priority: "high",
},
{
id: "TASK-5326",
title: "We need to hack the redundant UTF8 transmitter!",
status: "todo",
label: "bug",
priority: "low",
},
{
id: "TASK-6274",
title:
"Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
status: "canceled",
label: "documentation",
priority: "low",
},
{
id: "TASK-1571",
title:
"I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
status: "in progress",
label: "feature",
priority: "medium",
},
{
id: "TASK-9518",
title:
"Compressing the interface won't do anything, we need to compress the online SDD matrix!",
status: "canceled",
label: "documentation",
priority: "medium",
},
{
id: "TASK-5581",
title:
"I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
status: "backlog",
label: "documentation",
priority: "high",
},
{
id: "TASK-2197",
title:
"Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
status: "todo",
label: "documentation",
priority: "low",
},
{
id: "TASK-8484",
title: "We need to parse the solid state UDP firewall!",
status: "in progress",
label: "bug",
priority: "low",
},
{
id: "TASK-9892",
title:
"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",
},
{
id: "TASK-9616",
title: "We need to synthesize the cross-platform ASCII pixel!",
status: "in progress",
label: "feature",
priority: "medium",
},
{
id: "TASK-9744",
title:
"Use the back-end IP card, then you can input the solid state hard drive!",
status: "done",
label: "documentation",
priority: "low",
},
{
id: "TASK-1376",
title:
"Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
status: "backlog",
label: "documentation",
priority: "low",
},
{
id: "TASK-7382",
title:
"If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!",
status: "todo",
label: "feature",
priority: "low",
},
{
id: "TASK-2290",
title:
"I'll compress the virtual JSON panel, that should application the UTF8 bus!",
status: "canceled",
label: "documentation",
priority: "high",
},
{
id: "TASK-1533",
title:
"You can't input the firewall without overriding the wireless TCP firewall!",
status: "done",
label: "bug",
priority: "high",
},
{
id: "TASK-4920",
title:
"Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
status: "in progress",
label: "bug",
priority: "high",
},
{
id: "TASK-5168",
title:
"If we synthesize the bus, we can get to the IP panel through the virtual TLS array!",
status: "in progress",
label: "feature",
priority: "low",
},
{
id: "TASK-7103",
title: "We need to parse the multi-byte EXE bandwidth!",
status: "canceled",
label: "feature",
priority: "low",
},
{
id: "TASK-4314",
title:
"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",
},
{
id: "TASK-3415",
title:
"Use the cross-platform XML application, then you can quantify the solid state feed!",
status: "todo",
label: "feature",
priority: "high",
},
{
id: "TASK-8339",
title:
"Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
status: "in progress",
label: "feature",
priority: "low",
},
{
id: "TASK-6995",
title:
"Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
status: "todo",
label: "feature",
priority: "high",
},
{
id: "TASK-8053",
title:
"If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!",
status: "todo",
label: "feature",
priority: "medium",
},
{
id: "TASK-4336",
title:
"If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!",
status: "todo",
label: "documentation",
priority: "low",
},
{
id: "TASK-8790",
title:
"I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
status: "done",
label: "bug",
priority: "medium",
},
{
id: "TASK-8980",
title:
"Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
status: "canceled",
label: "bug",
priority: "low",
},
{
id: "TASK-7342",
title: "Use the neural CLI card, then you can parse the online port!",
status: "backlog",
label: "documentation",
priority: "low",
},
{
id: "TASK-5608",
title:
"I'll hack the haptic SSL program, that should bus the UDP transmitter!",
status: "canceled",
label: "documentation",
priority: "low",
},
{
id: "TASK-1606",
title:
"I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
status: "done",
label: "feature",
priority: "medium",
},
{
id: "TASK-7872",
title:
"Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
status: "canceled",
label: "feature",
priority: "medium",
},
{
id: "TASK-4167",
title:
"Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
status: "canceled",
label: "bug",
priority: "medium",
},
{
id: "TASK-9581",
title:
"You can't index the port without hacking the cross-platform XSS monitor!",
status: "backlog",
label: "documentation",
priority: "low",
},
{
id: "TASK-8806",
title: "We need to bypass the back-end SSL panel!",
status: "done",
label: "bug",
priority: "medium",
},
{
id: "TASK-6542",
title:
"Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
status: "done",
label: "feature",
priority: "low",
},
{
id: "TASK-6806",
title:
"The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!",
status: "canceled",
label: "documentation",
priority: "low",
},
{
id: "TASK-9549",
title: "You can't bypass the bus without connecting the neural JBOD bus!",
status: "todo",
label: "feature",
priority: "high",
},
{
id: "TASK-1075",
title:
"Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
status: "done",
label: "feature",
priority: "medium",
},
{
id: "TASK-1427",
title:
"Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
status: "done",
label: "documentation",
priority: "high",
},
{
id: "TASK-1907",
title:
"Hacking the circuit won't do anything, we need to back up the online DRAM system!",
status: "todo",
label: "documentation",
priority: "high",
},
{
id: "TASK-4309",
title:
"If we generate the system, we can get to the TCP sensor through the optical GB pixel!",
status: "backlog",
label: "bug",
priority: "medium",
},
{
id: "TASK-3973",
title:
"I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
status: "todo",
label: "feature",
priority: "medium",
},
{
id: "TASK-7962",
title:
"Use the wireless RAM program, then you can hack the cross-platform feed!",
status: "canceled",
label: "bug",
priority: "low",
},
{
id: "TASK-3360",
title:
"You can't quantify the program without synthesizing the neural OCR interface!",
status: "done",
label: "feature",
priority: "medium",
},
{
id: "TASK-9887",
title:
"Use the auxiliary ASCII sensor, then you can connect the solid state port!",
status: "backlog",
label: "bug",
priority: "medium",
},
{
id: "TASK-3649",
title:
"I'll input the virtual USB system, that should circuit the DNS monitor!",
status: "in progress",
label: "feature",
priority: "medium",
},
{
id: "TASK-3586",
title:
"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",
},
{
id: "TASK-5150",
title:
"I'll hack the wireless XSS port, that should transmitter the IP interface!",
status: "canceled",
label: "feature",
priority: "medium",
},
{
id: "TASK-3652",
title:
"The SQL interface is down, override the optical bus so we can program the ASCII interface!",
status: "backlog",
label: "feature",
priority: "low",
},
{
id: "TASK-6884",
title:
"Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
status: "canceled",
label: "feature",
priority: "high",
},
{
id: "TASK-1591",
title: "We need to connect the mobile XSS driver!",
status: "in progress",
label: "feature",
priority: "high",
},
{
id: "TASK-3802",
title:
"Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
status: "in progress",
label: "feature",
priority: "low",
},
{
id: "TASK-7253",
title:
"Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
status: "backlog",
label: "bug",
priority: "high",
},
{
id: "TASK-9739",
title: "We need to hack the multi-byte HDD bus!",
status: "done",
label: "documentation",
priority: "medium",
},
{
id: "TASK-4424",
title:
"Try to hack the HEX alarm, maybe it will connect the optical pixel!",
status: "in progress",
label: "documentation",
priority: "medium",
},
{
id: "TASK-3922",
title:
"You can't back up the capacitor without generating the wireless PCI program!",
status: "backlog",
label: "bug",
priority: "low",
},
{
id: "TASK-4921",
title:
"I'll index the open-source IP feed, that should system the GB application!",
status: "canceled",
label: "bug",
priority: "low",
},
{
id: "TASK-5814",
title: "We need to calculate the 1080p AGP feed!",
status: "backlog",
label: "bug",
priority: "high",
},
{
id: "TASK-2645",
title:
"Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
status: "todo",
label: "documentation",
priority: "medium",
},
{
id: "TASK-4535",
title:
"Try to copy the JSON circuit, maybe it will connect the wireless feed!",
status: "in progress",
label: "feature",
priority: "low",
},
{
id: "TASK-4463",
title: "We need to copy the solid state AGP monitor!",
status: "done",
label: "documentation",
priority: "low",
},
{
id: "TASK-9745",
title:
"If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!",
status: "canceled",
label: "feature",
priority: "high",
},
{
id: "TASK-2080",
title:
"If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!",
status: "todo",
label: "bug",
priority: "medium",
},
{
id: "TASK-3838",
title:
"I'll bypass the online TCP application, that should panel the AGP system!",
status: "backlog",
label: "bug",
priority: "high",
},
{
id: "TASK-1340",
title: "We need to navigate the virtual PNG circuit!",
status: "todo",
label: "bug",
priority: "medium",
},
{
id: "TASK-6665",
title:
"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",
},
{
id: "TASK-7585",
title:
"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",
},
{
id: "TASK-6319",
title: "We need to copy the multi-byte SCSI program!",
status: "backlog",
label: "bug",
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",
},
{
id: "TASK-9035",
title: "We need to override the solid state PNG array!",
status: "canceled",
label: "documentation",
priority: "low",
},
{
id: "TASK-3970",
title:
"You can't index the transmitter without quantifying the haptic ASCII card!",
status: "todo",
label: "documentation",
priority: "medium",
},
{
id: "TASK-4473",
title:
"You can't bypass the protocol without overriding the neural RSS program!",
status: "todo",
label: "documentation",
priority: "low",
},
{
id: "TASK-4136",
title:
"You can't hack the hard drive without hacking the primary JSON program!",
status: "canceled",
label: "bug",
priority: "medium",
},
{
id: "TASK-3939",
title:
"Use the back-end SQL firewall, then you can connect the neural hard drive!",
status: "done",
label: "feature",
priority: "low",
},
{
id: "TASK-2007",
title:
"I'll input the back-end USB protocol, that should bandwidth the PCI system!",
status: "backlog",
label: "bug",
priority: "high",
},
{
id: "TASK-7516",
title:
"Use the primary SQL program, then you can generate the auxiliary transmitter!",
status: "done",
label: "documentation",
priority: "medium",
},
{
id: "TASK-6906",
title:
"Try to back up the DRAM system, maybe it will reboot the online transmitter!",
status: "done",
label: "feature",
priority: "high",
},
{
id: "TASK-5207",
title:
"The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!",
status: "in progress",
label: "bug",
priority: "low",
},
];

View File

@ -1,42 +0,0 @@
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/columns";
import { DataTable } from "./components/data-table";
import { TasksDialogs } from "./components/tasks-dialogs";
import { TasksPrimaryButtons } from "./components/tasks-primary-buttons";
import TasksProvider from "./context/tasks-context";
import { tasks } from "./data/tasks";
export default function Tasks() {
return (
<TasksProvider>
<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 gap-x-4 space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">Tasks</h2>
<p className="text-muted-foreground">
Here&apos;s a list of your tasks for this month!
</p>
</div>
<TasksPrimaryButtons />
</div>
<div className="-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0">
<DataTable data={tasks} columns={columns} />
</div>
</Main>
<TasksDialogs />
</TasksProvider>
);
}

View File

@ -29,18 +29,6 @@ export function DataTableToolbar<TData>({
className="h-8 w-[150px] lg:w-[250px]"
/>
<div className="flex gap-x-2">
{table.getColumn("status") && (
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={[
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
{ label: "Invited", value: "invited" },
{ label: "Suspended", value: "suspended" },
]}
/>
)}
{table.getColumn("role") && (
<DataTableFacetedFilter
column={table.getColumn("role")}

View File

@ -1,4 +1,4 @@
import { IconMailPlus, IconUserPlus } from "@tabler/icons-react";
import { IconUserPlus } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { useUsers } from "../context/users-context";
@ -6,13 +6,6 @@ export function UsersPrimaryButtons() {
const { setOpen } = useUsers();
return (
<div className="flex gap-2">
<Button
variant="outline"
className="space-x-1"
onClick={() => setOpen("invite")}
>
<span>Invite User</span> <IconMailPlus size={18} />
</Button>
<Button className="space-x-1" onClick={() => setOpen("add")}>
<span>Add User</span> <IconUserPlus size={18} />
</Button>

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ export const Route = createFileRoute("/(auth)/sign-in")({
beforeLoad: async () => {
if (isLoggedIn()) {
throw redirect({
to: "/dashboard",
to: "/dashboard/shop",
});
}
},

View File

@ -0,0 +1,11 @@
import { createSamplePurchase } from '@/api/mock/data/create-purchase-data'
import { Button } from '@/components/ui/button'
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/_authenticated/dashboard/mock/')({
component: RouteComponent,
})
function RouteComponent() {
return <Button onClick={() => createSamplePurchase()}>New Fake Purchase</Button>
}

View File

@ -1,11 +1,9 @@
import RecentSales from "@/pages/sales/recent-sales";
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute(
"/_authenticated/dashboard/sales/recent-sales",
)({
component: RouteComponent,
component: RecentSales,
});
function RouteComponent() {
return <div>Recent Sales</div>;
}

View File

@ -1,8 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import SettingsAccount from "@/pages/settings/account";
export const Route = createLazyFileRoute(
"/_authenticated/dashboard/settings/account",
)({
component: SettingsAccount,
});

View File

@ -1,8 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import SettingsDisplay from "@/pages/settings/display";
export const Route = createLazyFileRoute(
"/_authenticated/dashboard/settings/display",
)({
component: SettingsDisplay,
});

View File

@ -1,8 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import SettingsNotifications from "@/pages/settings/notifications";
export const Route = createLazyFileRoute(
"/_authenticated/dashboard/settings/notifications",
)({
component: SettingsNotifications,
});

View File

@ -1,8 +0,0 @@
import SettingsSecurity from "@/pages/settings/security";
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute(
"/_authenticated/dashboard/settings/security",
)({
component: SettingsSecurity,
});

View File

@ -1,6 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Tasks from "@/pages/tasks";
export const Route = createLazyFileRoute("/_authenticated/dashboard/tasks/")({
component: Tasks,
});

View File

@ -1,13 +1,13 @@
import jwt_decode from "jwt-decode";
// import jwt_decode from "jwt-decode";
export function isTokenValid(token: string | null): boolean {
if (!token) return false;
try {
const decoded: { exp: number } = jwt_decode(token);
const now = Date.now() / 1000;
return decoded.exp > now;
} catch (e) {
console.warn("Invalid token format:", e);
return false;
}
}
// export function isTokenValid(token: string | null): boolean {
// if (!token) return false;
// try {
// const decoded: { exp: number } = jwt_decode(token);
// const now = Date.now() / 1000;
// return decoded.exp > now;
// } catch (e) {
// console.warn("Invalid token format:", e);
// return false;
// }
// }