Final commit (most likely)
This commit is contained in:
parent
f2af9dc566
commit
6903a8deb0
@ -1,25 +1,41 @@
|
|||||||
# Base Image
|
FROM python:3.12
|
||||||
FROM python:3.13
|
|
||||||
|
|
||||||
RUN pip install poetry
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
# Copy dependency files first to leverage caching
|
# Install uv
|
||||||
COPY pyproject.toml poetry.lock /app/
|
# 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
|
COPY ./app /app/app
|
||||||
|
|
||||||
# Ensure dependencies are installed correctly
|
# Sync the project
|
||||||
RUN poetry install --no-interaction --no-ansi --without dev
|
# 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
|
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Command to run the app
|
|
||||||
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
|
@ -1,68 +1,55 @@
|
|||||||
|
version: "3.9"
|
||||||
services:
|
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:
|
db:
|
||||||
image: postgres:12
|
image: postgres:16
|
||||||
restart: no
|
container_name: swagshop-postgres
|
||||||
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: swagshop
|
||||||
volumes:
|
volumes:
|
||||||
- app-db-data:/var/lib/postgresql/data/pgdata
|
- pgdata:/var/lib/postgresql/data
|
||||||
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
|
|
||||||
networks:
|
networks:
|
||||||
- default
|
- internal
|
||||||
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
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app-db-data:
|
pgdata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
driver: bridge
|
||||||
|
2
frontend/.dockerignore
Normal file
2
frontend/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
@ -1 +1,2 @@
|
|||||||
VITE_API_URL=
|
VITE_API_URL=http://localhost:8000
|
||||||
|
VITE_USE_MOCK_API=true
|
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal 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"]
|
63
frontend/src/api/mock/data/create-purchase-data.ts
Normal file
63
frontend/src/api/mock/data/create-purchase-data.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
@ -31,7 +31,7 @@ class MockDB extends Dexie {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super("MockDB");
|
super("MockDB");
|
||||||
this.version(1).stores({
|
this.version(1).stores({
|
||||||
users: "++id,username,email,uuid",
|
users: "++id,username,email,uuid,user_role",
|
||||||
preferences: "user_id",
|
preferences: "user_id",
|
||||||
statistics: "user_id",
|
statistics: "user_id",
|
||||||
shops: "++id,uuid,name",
|
shops: "++id,uuid,name",
|
||||||
|
@ -86,7 +86,7 @@ export interface ProductCreate {
|
|||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: number;
|
||||||
stock_quantity: number;
|
stock_quantity: number;
|
||||||
image_data: string | undefined;
|
image_data?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductWithDetails extends Product {
|
export interface ProductWithDetails extends Product {
|
||||||
|
@ -55,10 +55,6 @@ export const MockProductAPI = {
|
|||||||
.where("product_id")
|
.where("product_id")
|
||||||
.equals(productId)
|
.equals(productId)
|
||||||
.toArray();
|
.toArray();
|
||||||
const variants = await mockDB.product_variants
|
|
||||||
.where("product_id")
|
|
||||||
.equals(productId)
|
|
||||||
.toArray();
|
|
||||||
const categoryLinks = await mockDB.product_category_junctions
|
const categoryLinks = await mockDB.product_category_junctions
|
||||||
.where("product_id")
|
.where("product_id")
|
||||||
.equals(productId)
|
.equals(productId)
|
||||||
@ -76,7 +72,6 @@ export const MockProductAPI = {
|
|||||||
return {
|
return {
|
||||||
...product,
|
...product,
|
||||||
images,
|
images,
|
||||||
variants,
|
|
||||||
categories,
|
categories,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,7 @@ export const MockPurchaseAPI = {
|
|||||||
|
|
||||||
async createPurchase(
|
async createPurchase(
|
||||||
purchase: Omit<Purchase, "id">,
|
purchase: Omit<Purchase, "id">,
|
||||||
entries: Omit<PurchaseEntry, "id">[],
|
entries: Omit<PurchaseEntry, "id">[] = [],
|
||||||
): Promise<Purchase> {
|
): Promise<Purchase> {
|
||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
const newPurchase: Purchase = {
|
const newPurchase: Purchase = {
|
||||||
@ -63,17 +63,31 @@ export const MockPurchaseAPI = {
|
|||||||
|
|
||||||
await mockDB.purchases.add(newPurchase);
|
await mockDB.purchases.add(newPurchase);
|
||||||
|
|
||||||
await mockDB.purchase_entries.bulkAdd(
|
if (entries.length > 0) {
|
||||||
entries.map((entry) => ({
|
await mockDB.purchase_entries.bulkAdd(
|
||||||
...entry,
|
entries.map((entry) => ({
|
||||||
purchase_id: id,
|
...entry,
|
||||||
id: Date.now() + Math.random(),
|
purchase_id: id,
|
||||||
})),
|
id: Date.now() + Math.random(),
|
||||||
);
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return newPurchase;
|
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> {
|
async deletePurchase(purchaseId: number): Promise<boolean> {
|
||||||
await mockDB.purchase_entries
|
await mockDB.purchase_entries
|
||||||
.where("purchase_id")
|
.where("purchase_id")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const currencies = [
|
export const currencies = [
|
||||||
"AED",
|
"AED",
|
||||||
"AFN",
|
"AFN",
|
||||||
"ALL",
|
"ALL",
|
||||||
|
@ -1,27 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarHeader,
|
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { NavGroup } from "@/components/layout/nav-group";
|
import { NavGroup } from "@/components/layout/nav-group";
|
||||||
import { sidebarData } from "./data/sidebar-data";
|
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>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" variant="floating" {...props}>
|
<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>
|
<SidebarContent>
|
||||||
{sidebarData.navGroups.map((props) => (
|
{sidebarData.navGroups.map((props) => (
|
||||||
<NavGroup key={props.title} {...props} />
|
<NavGroup key={props.title} {...props} />
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import {
|
import
|
||||||
IconBrowserCheck,
|
{
|
||||||
IconBuildingStore,
|
IconBuildingStore,
|
||||||
IconClipboardCheckFilled,
|
IconClipboardCheckFilled,
|
||||||
IconCoin,
|
IconCoin, IconPackage,
|
||||||
IconForklift,
|
IconPalette,
|
||||||
IconHelp,
|
IconSettings,
|
||||||
IconLayoutDashboard,
|
IconTag,
|
||||||
IconNotification,
|
IconUserCog,
|
||||||
IconPackage,
|
IconUsers
|
||||||
IconPalette,
|
} from "@tabler/icons-react";
|
||||||
IconSettings,
|
|
||||||
IconTag,
|
|
||||||
IconTool,
|
|
||||||
IconUserCog,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { type SidebarData } from "../types";
|
import { type SidebarData } from "../types";
|
||||||
|
|
||||||
export const sidebarData: SidebarData = {
|
export const sidebarData: SidebarData = {
|
||||||
@ -22,11 +16,11 @@ export const sidebarData: SidebarData = {
|
|||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
items: [
|
items: [
|
||||||
{
|
// {
|
||||||
title: "Dashboard",
|
// title: "Dashboard",
|
||||||
url: "/dashboard",
|
// url: "/dashboard",
|
||||||
icon: IconLayoutDashboard,
|
// icon: IconLayoutDashboard,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: "Shop",
|
title: "Shop",
|
||||||
url: "/dashboard/shop",
|
url: "/dashboard/shop",
|
||||||
@ -37,11 +31,6 @@ export const sidebarData: SidebarData = {
|
|||||||
url: "/dashboard/products",
|
url: "/dashboard/products",
|
||||||
icon: IconPackage,
|
icon: IconPackage,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Inventory",
|
|
||||||
url: "/dashboard/tasks",
|
|
||||||
icon: IconForklift,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Sales",
|
title: "Sales",
|
||||||
icon: IconCoin,
|
icon: IconCoin,
|
||||||
@ -77,33 +66,14 @@ export const sidebarData: SidebarData = {
|
|||||||
url: "/dashboard/settings",
|
url: "/dashboard/settings",
|
||||||
icon: IconUserCog,
|
icon: IconUserCog,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Account",
|
|
||||||
url: "/dashboard/settings/account",
|
|
||||||
icon: IconTool,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Appearance",
|
title: "Appearance",
|
||||||
url: "/dashboard/settings/appearance",
|
url: "/dashboard/settings/appearance",
|
||||||
icon: IconPalette,
|
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,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -18,9 +18,9 @@ const MainNavbar = () => {
|
|||||||
<li className="font-medium text-primary">
|
<li className="font-medium text-primary">
|
||||||
<a href="#home">Home</a>
|
<a href="#home">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{/* <li>
|
||||||
<a href="#pricing">Pricing</a>
|
<a href="#pricing">Pricing</a>
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
<li>
|
||||||
<a href="#faqs">FAQs</a>
|
<a href="#faqs">FAQs</a>
|
||||||
</li>
|
</li>
|
||||||
@ -43,9 +43,9 @@ const MainNavbar = () => {
|
|||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a href="#features">Features</a>
|
<a href="#features">Features</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
{/* <DropdownMenuItem>
|
||||||
<a href="#pricing">Pricing</a>
|
<a href="#pricing">Pricing</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem> */}
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a href="#faqs">FAQs</a>
|
<a href="#faqs">FAQs</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -45,12 +45,6 @@ export function ProfileDropdown() {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<Link to="/dashboard/settings">
|
|
||||||
Profile
|
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/dashboard/settings">
|
<Link to="/dashboard/settings">
|
||||||
Settings
|
Settings
|
||||||
|
@ -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 };
|
|
@ -1,9 +1,14 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { PurchaseWithDetails } from "@/api/mock/models";
|
import {
|
||||||
|
Purchase,
|
||||||
|
PurchaseWithDetails,
|
||||||
|
} from "@/api/mock/models";
|
||||||
import { purchaseAPI } from "@/api/api";
|
import { purchaseAPI } from "@/api/api";
|
||||||
|
|
||||||
export function usePurchase(purchaseId?: number) {
|
export function usePurchase(purchaseId?: number) {
|
||||||
return useQuery<PurchaseWithDetails | null>({
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const purchase = useQuery<PurchaseWithDetails | null>({
|
||||||
queryKey: ["purchase", purchaseId],
|
queryKey: ["purchase", purchaseId],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (purchaseId === undefined) return Promise.resolve(null);
|
if (purchaseId === undefined) return Promise.resolve(null);
|
||||||
@ -11,4 +16,34 @@ export function usePurchase(purchaseId?: number) {
|
|||||||
},
|
},
|
||||||
enabled: purchaseId !== undefined,
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import MainNavbar from "@/components/main-navbar";
|
import MainNavbar from "@/components/main-navbar";
|
||||||
import Features from "./components/features";
|
import Features from "./components/features";
|
||||||
import Hero from "./components/hero";
|
import Hero from "./components/hero";
|
||||||
import Pricing from "./components/pricing";
|
// import Pricing from "./components/pricing";
|
||||||
import Faq from "./components/faq";
|
import Faq from "./components/faq";
|
||||||
import CallToAction from "./components/call-to-action";
|
import CallToAction from "./components/call-to-action";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export default function MainPage() {
|
|||||||
<div className="h-[20vh] min-h-[20vh]"></div>
|
<div className="h-[20vh] min-h-[20vh]"></div>
|
||||||
<Hero />
|
<Hero />
|
||||||
<Features />
|
<Features />
|
||||||
<Pricing />
|
{/* <Pricing /> */}
|
||||||
<Faq />
|
<Faq />
|
||||||
<CallToAction />
|
<CallToAction />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
description: z.string().min(1, "Description is required"),
|
description: z.string().min(1, "Description is required"),
|
||||||
@ -18,12 +19,11 @@ type ProductForm = z.infer<typeof schema>;
|
|||||||
export function ProductForm({
|
export function ProductForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
form,
|
form,
|
||||||
imagePreview,
|
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
submitLabel,
|
submitLabel,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
form: UseFormReturn<typeof ProductForm>;
|
form: UseFormReturn<ProductForm>;
|
||||||
imagePreview?: string;
|
imagePreview?: string;
|
||||||
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
|
@ -96,6 +96,7 @@ export const columns: ColumnDef<Coupon>[] = [
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
filterFn: (row, id, value) => {
|
filterFn: (row, id, value) => {
|
||||||
|
console.log(id);
|
||||||
const validDue = new Date(row.original.valid_due);
|
const validDue = new Date(row.original.valid_due);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const status = isBefore(validDue, now) ? "expired" : "active";
|
const status = isBefore(validDue, now) ? "expired" : "active";
|
||||||
|
@ -27,9 +27,9 @@ export function DataTableToolbar<TData>({
|
|||||||
className="h-8 w-[150px] lg:w-[250px]"
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
{table.getColumn("valid_due") && (
|
{table.getColumn("Valid Due") && (
|
||||||
<DataTableFacetedFilter
|
<DataTableFacetedFilter
|
||||||
column={table.getColumn("valid_due")}
|
column={table.getColumn("Valid Due")}
|
||||||
title="Status"
|
title="Status"
|
||||||
options={couponTypes.map((t) => ({ ...t }))}
|
options={couponTypes.map((t) => ({ ...t }))}
|
||||||
/>
|
/>
|
||||||
|
@ -24,8 +24,6 @@ export default function Coupons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(couponList);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CouponsProvider>
|
<CouponsProvider>
|
||||||
<Header fixed>
|
<Header fixed>
|
||||||
|
@ -37,12 +37,11 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||||
const facets = column?.getFacetedUniqueValues();
|
const facets = column?.getFacetedUniqueValues();
|
||||||
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
<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}
|
{title}
|
||||||
{selectedValues?.size > 0 && (
|
{selectedValues?.size > 0 && (
|
||||||
<>
|
<>
|
||||||
@ -104,7 +103,7 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
isSelected
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "opacity-50 [&_svg]:invisible",
|
: "opacity-50 [&_svg]:invisible",
|
||||||
@ -113,7 +112,7 @@ export function DataTableFacetedFilter<TData, TValue>({
|
|||||||
<CheckIcon className={cn("h-4 w-4")} />
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
</div>
|
</div>
|
||||||
{option.icon && (
|
{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>
|
<span>{option.label}</span>
|
||||||
{facets?.get(option.value) && (
|
{facets?.get(option.value) && (
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -2,9 +2,9 @@ import { Cross2Icon } from "@radix-ui/react-icons";
|
|||||||
import { Table } from "@tanstack/react-table";
|
import { Table } from "@tanstack/react-table";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { DataTableViewOptions } from "../components/data-table-view-options";
|
import { couponTypes } from "../data/data";
|
||||||
import { priorities, statuses } from "../data/data";
|
|
||||||
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
|
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
|
||||||
|
import { DataTableViewOptions } from "./data-table-view-options";
|
||||||
|
|
||||||
interface DataTableToolbarProps<TData> {
|
interface DataTableToolbarProps<TData> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
@ -19,26 +19,19 @@ export function DataTableToolbar<TData>({
|
|||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter tasks..."
|
placeholder="Filter coupons..."
|
||||||
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
table.getColumn("title")?.setFilterValue(event.target.value)
|
table.getColumn("name")?.setFilterValue(event.target.value)
|
||||||
}
|
}
|
||||||
className="h-8 w-[150px] lg:w-[250px]"
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
{table.getColumn("status") && (
|
{table.getColumn("valid_due") && (
|
||||||
<DataTableFacetedFilter
|
<DataTableFacetedFilter
|
||||||
column={table.getColumn("status")}
|
column={table.getColumn("valid_due")}
|
||||||
title="Status"
|
title="Status"
|
||||||
options={statuses}
|
options={couponTypes.map((t) => ({ ...t }))}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{table.getColumn("priority") && (
|
|
||||||
<DataTableFacetedFilter
|
|
||||||
column={table.getColumn("priority")}
|
|
||||||
title="Priority"
|
|
||||||
options={priorities}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
@ -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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import * as React from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
|
RowData,
|
||||||
SortingState,
|
SortingState,
|
||||||
VisibilityState,
|
VisibilityState,
|
||||||
flexRender,
|
flexRender,
|
||||||
@ -21,25 +22,27 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { DataTablePagination } from "../components/data-table-pagination";
|
import { Purchase } from "../data/schema";
|
||||||
import { DataTableToolbar } from "../components/data-table-toolbar";
|
import { DataTablePagination } from "./data-table-pagination";
|
||||||
|
import { DataTableToolbar } from "./data-table-toolbar";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
declare module "@tanstack/react-table" {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
data: TData[];
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
interface DataTableProps {
|
||||||
columns,
|
columns: ColumnDef<Purchase>[];
|
||||||
data,
|
data: Purchase[];
|
||||||
}: DataTableProps<TData, TValue>) {
|
}
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
|
||||||
const [columnVisibility, setColumnVisibility] =
|
export function PurchasesTable({ columns, data }: DataTableProps) {
|
||||||
React.useState<VisibilityState>({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
[],
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@ -70,31 +73,37 @@ export function DataTable<TData, TValue>({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id} className="group/row">
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => (
|
||||||
return (
|
<TableHead
|
||||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
key={header.id}
|
||||||
{header.isPlaceholder
|
colSpan={header.colSpan}
|
||||||
? null
|
className={header.column.columnDef.meta?.className ?? ""}
|
||||||
: flexRender(
|
>
|
||||||
header.column.columnDef.header,
|
{header.isPlaceholder
|
||||||
header.getContext(),
|
? null
|
||||||
)}
|
: flexRender(
|
||||||
</TableHead>
|
header.column.columnDef.header,
|
||||||
);
|
header.getContext(),
|
||||||
})}
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{table.getRowModel().rows?.length ? (
|
{table.getRowModel().rows.length ? (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={row.id}
|
key={row.id}
|
||||||
data-state={row.getIsSelected() && "selected"}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
className="group/row"
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell key={cell.id}>
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
className={cell.column.columnDef.meta?.className ?? ""}
|
||||||
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
cell.column.columnDef.cell,
|
cell.column.columnDef.cell,
|
||||||
cell.getContext(),
|
cell.getContext(),
|
@ -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;
|
||||||
|
};
|
25
frontend/src/pages/sales/recent-sales/data/data.ts
Normal file
25
frontend/src/pages/sales/recent-sales/data/data.ts
Normal 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;
|
13
frontend/src/pages/sales/recent-sales/data/schema.ts
Normal file
13
frontend/src/pages/sales/recent-sales/data/schema.ts
Normal 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);
|
75
frontend/src/pages/sales/recent-sales/index.tsx
Normal file
75
frontend/src/pages/sales/recent-sales/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,11 +1,9 @@
|
|||||||
import { Outlet } from "@tanstack/react-router";
|
import { Outlet } from "@tanstack/react-router";
|
||||||
import {
|
import
|
||||||
IconBrowserCheck,
|
{
|
||||||
IconLock,
|
IconLock, IconPalette,
|
||||||
IconNotification,
|
IconUser
|
||||||
IconPalette,
|
} from "@tabler/icons-react";
|
||||||
IconUser,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { Main } from "@/components/layout/main";
|
import { Main } from "@/components/layout/main";
|
||||||
@ -64,15 +62,5 @@ const sidebarNavItems = [
|
|||||||
title: "Appearance",
|
title: "Appearance",
|
||||||
icon: <IconPalette size={18} />,
|
icon: <IconPalette size={18} />,
|
||||||
href: "/dashboard/settings/appearance",
|
href: "/dashboard/settings/appearance",
|
||||||
},
|
}
|
||||||
{
|
|
||||||
title: "Notifications",
|
|
||||||
icon: <IconNotification size={18} />,
|
|
||||||
href: "/dashboard/settings/notifications",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Display",
|
|
||||||
icon: <IconBrowserCheck size={18} />,
|
|
||||||
href: "/dashboard/settings/display",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -17,6 +17,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { IconBuildingStore, IconClock, IconLink } from "@tabler/icons-react";
|
import { IconBuildingStore, IconClock, IconLink } from "@tabler/icons-react";
|
||||||
|
|
||||||
export default function ShopSidebar({ ...props }) {
|
export default function ShopSidebar({ ...props }) {
|
||||||
|
console.log(props);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [val, setVal] = useState(pathname ?? "/settings");
|
const [val, setVal] = useState(pathname ?? "/settings");
|
||||||
|
@ -4,7 +4,7 @@ import { ProfileDropdown } from "@/components/profile-dropdown";
|
|||||||
import { ThemeSwitch } from "@/components/theme-switch";
|
import { ThemeSwitch } from "@/components/theme-switch";
|
||||||
import { ShopAboutForm } from "./components/shop-about-form";
|
import { ShopAboutForm } from "./components/shop-about-form";
|
||||||
import { Search } from "@/components/search";
|
import { Search } from "@/components/search";
|
||||||
import ShopSidebar from "./components/shop-sidebar";
|
// import ShopSidebar from "./components/shop-sidebar";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
@ -30,7 +30,7 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="my-4 flex flex-row gap-4">
|
<div className="my-4 flex flex-row gap-4">
|
||||||
<ShopSidebar />
|
{/* <ShopSidebar /> */}
|
||||||
|
|
||||||
<ShopAboutForm />
|
<ShopAboutForm />
|
||||||
</div>
|
</div>
|
||||||
|
@ -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} />,
|
|
||||||
},
|
|
||||||
];
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
@ -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>;
|
|
@ -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",
|
|
||||||
},
|
|
||||||
];
|
|
@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -29,18 +29,6 @@ export function DataTableToolbar<TData>({
|
|||||||
className="h-8 w-[150px] lg:w-[250px]"
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-x-2">
|
<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") && (
|
{table.getColumn("role") && (
|
||||||
<DataTableFacetedFilter
|
<DataTableFacetedFilter
|
||||||
column={table.getColumn("role")}
|
column={table.getColumn("role")}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IconMailPlus, IconUserPlus } from "@tabler/icons-react";
|
import { IconUserPlus } from "@tabler/icons-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useUsers } from "../context/users-context";
|
import { useUsers } from "../context/users-context";
|
||||||
|
|
||||||
@ -6,13 +6,6 @@ export function UsersPrimaryButtons() {
|
|||||||
const { setOpen } = useUsers();
|
const { setOpen } = useUsers();
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<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")}>
|
<Button className="space-x-1" onClick={() => setOpen("add")}>
|
||||||
<span>Add User</span> <IconUserPlus size={18} />
|
<span>Add User</span> <IconUserPlus size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ export const Route = createFileRoute("/(auth)/sign-in")({
|
|||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/dashboard",
|
to: "/dashboard/shop",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
|
import RecentSales from "@/pages/sales/recent-sales";
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createLazyFileRoute(
|
export const Route = createLazyFileRoute(
|
||||||
"/_authenticated/dashboard/sales/recent-sales",
|
"/_authenticated/dashboard/sales/recent-sales",
|
||||||
)({
|
)({
|
||||||
component: RouteComponent,
|
component: RecentSales,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Recent Sales</div>;
|
|
||||||
}
|
|
||||||
|
@ -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,
|
|
||||||
});
|
|
@ -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,
|
|
||||||
});
|
|
@ -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,
|
|
||||||
});
|
|
@ -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,
|
|
||||||
});
|
|
@ -1,6 +0,0 @@
|
|||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import Tasks from "@/pages/tasks";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/_authenticated/dashboard/tasks/")({
|
|
||||||
component: Tasks,
|
|
||||||
});
|
|
@ -1,13 +1,13 @@
|
|||||||
import jwt_decode from "jwt-decode";
|
// import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
export function isTokenValid(token: string | null): boolean {
|
// export function isTokenValid(token: string | null): boolean {
|
||||||
if (!token) return false;
|
// if (!token) return false;
|
||||||
try {
|
// try {
|
||||||
const decoded: { exp: number } = jwt_decode(token);
|
// const decoded: { exp: number } = jwt_decode(token);
|
||||||
const now = Date.now() / 1000;
|
// const now = Date.now() / 1000;
|
||||||
return decoded.exp > now;
|
// return decoded.exp > now;
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.warn("Invalid token format:", e);
|
// console.warn("Invalid token format:", e);
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user