Skip to content

Commit

Permalink
Merge pull request #56 from Prodeko:admin-ux-improvements
Browse files Browse the repository at this point in the history
Admin-ux-improvements
  • Loading branch information
ccruzkauppila authored Jan 12, 2025
2 parents d71b801 + 968e297 commit 373ec0f
Show file tree
Hide file tree
Showing 22 changed files with 129 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.


ALTER TYPE "ProductCategory" ADD VALUE 'CANDY';
ALTER TYPE "ProductCategory" ADD VALUE 'OTHER';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;
2 changes: 1 addition & 1 deletion prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
3 changes: 3 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ model Product {
category ProductCategory
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
ProductInventory ProductInventory[]
Prices ProductPrice[]
TransactionItem TransactionItem[]
Expand Down Expand Up @@ -190,6 +191,8 @@ enum ProductCategory {
FOOD
DRINK
SNACK
CANDY
OTHER
}

enum Role {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const AdminProductSection = ({ products }: Props) => {

return (
<section className="flex flex-col gap-3">
<div className="tex-lg flex w-full flex-col items-start justify-between gap-4 px-5 text-neutral-800 md:flex-row md:items-center md:gap-6 md:px-12 md:text-xl">
<div className="flex w-full flex-col items-start justify-between gap-4 px-5 text-sm text-neutral-800 md:flex-row md:items-center md:gap-6 md:px-12 md:text-xl">
<span className="flex-none text-neutral-500">
Displaying {filteredProducts.length} of {products.length} products
</span>
Expand Down
8 changes: 4 additions & 4 deletions src/app/(admin)/admin/edit-products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { AdminTitle } from "@/components/ui/AdminTitle";
import { getClientProducts } from "@/server/db/queries/product";
import { getActiveClientProducts } from "@/server/db/queries/product";

import { AdminProductSection } from "./AdminProductSection";

const Restock = async () => {
const products = await getClientProducts();
const products = await getActiveClientProducts();
return (
<div className="no-scrollbar flex h-fit w-full max-w-screen-lg flex-col gap-8 overflow-y-scroll px-0 lg:w-[80%]">
<AdminTitle withBackButton title="Products" />
<div className="no-scrollbar flex h-fit w-full max-w-screen-lg flex-col gap-4 overflow-y-scroll px-0 md:gap-8 lg:w-[80%]">
<AdminTitle title="Products" />
<AdminProductSection products={products} />
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(admin)/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export default async function AdminLayout({
const isSuperadmin = session?.user?.role === "SUPERADMIN";

return (
<div className="relative flex min-h-0 w-full flex-1 ">
<div className="relative flex w-full flex-1 flex-col-reverse overflow-hidden landscape:flex-row">
<AdminSidebar superadmin={isSuperadmin} />
<div className="flex h-full min-h-0 w-full flex-1 justify-center overflow-y-scroll pt-6">
<div className="flex h-full min-h-0 w-full flex-1 justify-center overflow-y-scroll py-6">
{children}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/admin/wishes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const WishAdmin = async () => {
const wishlist = await getWishes();

return (
<div className="flex w-full max-w-screen-lg flex-col gap-8">
<div className="flex w-full max-w-screen-lg flex-col gap-4 md:gap-8">
<AdminTitle title="Customer wishes" />
<span className="px-5 md:px-12">
<CustomerWishes admin initialWishlist={wishlist} />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(admin)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Props {

const Home = ({ children }: Props) => {
return (
<main className="flex max-h-dvh min-h-screen flex-col bg-neutral-50">
<main className="flex h-dvh flex-col bg-neutral-50">
<AdminHeader />
{children}
</main>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(loggedin)/shop/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { shopCatalogueID } from "@/common/constants";
import { ProductSection } from "@/components/ui/ProductSection";
import { PurchaseSlider } from "@/components/ui/PurchaseSlider";
import { ShoppingCart } from "@/components/ui/ShoppingCart";
import { getClientProducts } from "@/server/db/queries/product";
import { getActiveClientProducts } from "@/server/db/queries/product";
import { sections } from "@/state/activeSection";

import { FeaturedSection } from "./FeaturedSection";
import { ShopNav } from "./ShopNav";

const Shop = async () => {
const products = await getClientProducts();
const products = await getActiveClientProducts();
const drinks = products.filter((product) => product.category === "DRINK");
const snacks = products.filter((product) => product.category === "SNACK");
const food = products.filter((product) => product.category === "FOOD");
Expand Down
5 changes: 3 additions & 2 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from "zod";

import { ProductCategory } from "@prisma/client";

// Basetypes
export const IdParser = z
.number()
Expand All @@ -8,8 +10,7 @@ export const IdParser = z
export type Id = z.infer<typeof IdParser>;

// Product
export const ProductCategoryParser = z.enum(["FOOD", "DRINK", "SNACK"]);
export type ProductCategory = z.infer<typeof ProductCategoryParser>;
export const ProductCategoryParser = z.nativeEnum(ProductCategory);

export const ClientProductParser = z.object({ id: IdParser }).extend({
name: z.string().max(50, { message: "Name must be at most 50 characters" }),
Expand Down
29 changes: 16 additions & 13 deletions src/components/ui/AdminSidebar/SidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,32 @@ import { usePathname } from "next/navigation";
import { type ComponentProps } from "react";
import { IconType } from "react-icons";

const buttonStyles = cva("flex items-center justify-between px-9 py-6", {
variants: {
active: {
true: "bg-primary-400",
const buttonStyles = cva(
"flex w-full flex-col-reverse items-center justify-center px-3 py-3 portrait:gap-1 landscape:flex-row landscape:justify-between landscape:px-9 landscape:py-6",
{
variants: {
active: {
true: "",
},
},
},
});
);

const iconStyles = cva("", {
const iconStyles = cva("text-2xl landscape:text-4xl", {
variants: {
active: {
true: "text-white",
false: "text-primary-400",
true: "text-primary-400",
false: "text-neutral-400",
locked: "text-neutral-300",
},
},
});

const spanStyles = cva("text-xl 2xl:text-2xl", {
const spanStyles = cva("text-center text-xs 2xl:text-2xl landscape:text-left", {
variants: {
active: {
true: "font-medium text-white",
false: "font-normal text-neutral-700",
true: "font-medium text-primary-500",
false: "font-normal text-neutral-500 ",
locked: "cursor-not-allowed select-none font-normal text-neutral-400",
},
},
Expand Down Expand Up @@ -56,15 +59,15 @@ export const SidebarItem = ({
return (
<div className={buttonStyles({ active: false })}>
<span className={spanStyles({ active: "locked" })}>{text}</span>
<Icon size={36} className={iconStyles({ active: "locked" })} />
<Icon className={iconStyles({ active: "locked" })} />
</div>
);
}

return (
<Link href={href} className={buttonStyles({ active })}>
<span className={spanStyles({ active })}>{text}</span>
<Icon size={36} className={iconStyles({ active })} />
<Icon className={iconStyles({ active })} />
</Link>
);
};
41 changes: 8 additions & 33 deletions src/components/ui/AdminSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState } from "react";
import { FaCrown, FaLock } from "react-icons/fa6";
import {
HiChartBar,
HiChevronLeft,
HiChevronRight,
HiOutlinePlusCircle,
Expand All @@ -17,55 +18,29 @@ interface Props {
}

export const AdminSidebar = ({ superadmin }: Props) => {
const [visible, setVisible] = useState(false);

const ToggleButton = () => {
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div
className="w-fit p-4 text-3xl text-neutral-800 md:p-6 md:text-4xl "
onClick={() => setVisible(!visible)}
>
{visible ? <HiChevronLeft /> : <HiChevronRight />}
</div>
);
};
if (!visible)
return (
<div className="absolute bottom-0 left-0 z-20 rounded-2xl bg-white shadow-md">
<ToggleButton />
</div>
);

return (
<div className="absolute z-20 flex h-full w-80 flex-none flex-col justify-between gap-0 bg-white drop-shadow-md 2xl:w-96">
<div className="flex flex-col gap-0">
<div className="z-20 flex w-full flex-none gap-0 bg-white drop-shadow-md 2xl:w-96 landscape:h-full landscape:w-80 ">
<div className="flex w-full flex-row justify-between gap-0 landscape:flex-col landscape:justify-start">
<SidebarItem
text="Edit products"
text="Products"
Icon={HiShoppingCart}
href="/admin/edit-products"
/>
<SidebarItem
text="Add new product"
text="New"
Icon={HiOutlinePlusCircle}
href="/admin/newProduct"
/>
<SidebarItem
text="Customer wishes"
Icon={HiSparkles}
href="/admin/wishes"
/>
{superadmin ? (
<SidebarItem text="Wishes" Icon={HiSparkles} href="/admin/wishes" />
<SidebarItem text="Stats" Icon={HiChartBar} href="/admin/statistics" />
{superadmin && (
<SidebarItem
text="Superadmin"
Icon={FaCrown}
href="/admin/superadmin"
/>
) : (
<SidebarItem text="Superadmin" Icon={FaLock} href="" unavailable />
)}
</div>
<ToggleButton />
</div>
);
};
4 changes: 2 additions & 2 deletions src/components/ui/AdminTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ export const AdminTitle = ({
>
{withBackButton && (
<HiArrowLeft
className="text-lg md:text-3xl"
className="text-lg md:text-2xl"
onClick={() => router.back()}
/>
)}
<h2
className={cn(
"sticky top-0 z-10 bg-neutral-50 text-2xl font-semibold text-neutral-700 md:text-4xl 2xl:text-5xl",
"sticky top-0 z-10 bg-neutral-50 text-xl font-semibold text-neutral-700 md:text-2xl 2xl:text-4xl",
className,
)}
{...props}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/DropdownSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
import * as Select from "@radix-ui/react-select";

interface Props<T extends string> extends ComponentPropsWithoutRef<"select"> {
choices: NonEmptyArray<T>;
choices: ReadonlyArray<T>;
value?: T;
defaultValue?: T;
placeholder?: string;
Expand Down Expand Up @@ -40,7 +40,7 @@ export const DropdownSelect = <T extends string>({
<HiChevronDown size={25} />
</Select.Icon>
</Select.Trigger>
<Select.Content className="overflow-hidden rounded-xl border-2 bg-white shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)]">
<Select.Content className="z-10 overflow-hidden rounded-xl border-2 bg-white shadow-[0px_10px_38px_-10px_rgba(22,_23,_24,_0.35),0px_10px_20px_-15px_rgba(22,_23,_24,_0.2)]">
<Select.Viewport className="rounded-xl">
{choices.map((choice) => (
<Select.Item
Expand Down
46 changes: 41 additions & 5 deletions src/components/ui/EditProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,34 @@

import { useActionState, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { HiUserAdd } from "react-icons/hi";
import { HiTrash, HiUserAdd } from "react-icons/hi";

import type { ClientProduct, UpdateProductFormState } from "@/common/types";
import {
type ClientProduct,
type UpdateProductFormState,
} from "@/common/types";
import { DropdownSelect } from "@/components/ui/DropdownSelect";
import { InputWithLabel } from "@/components/ui/Input";
import { createProductAction } from "@/server/actions/admin/createProduct";
import { deactivateProduct } from "@/server/actions/admin/deactivateProduct";
import { ProductCategory } from "@prisma/client";

import { ButtonGroup } from "./Buttons/ButtonGroup";
import { FatButton } from "./Buttons/FatButton";
import { ThinButton } from "./Buttons/ThinButton";
import { ImageUpload } from "./ImageUpload";

interface Props {
// Autofills the form if a product is given
product?: ClientProduct;
}

const productCategories = Object.values(ProductCategory);
const categoriesFormatted = productCategories.map(
(category) =>
category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(),
);

export const EditProductForm = ({ product }: Props) => {
const [state, formAction, isPending] = useActionState<
UpdateProductFormState,
Expand Down Expand Up @@ -49,7 +61,6 @@ export const EditProductForm = ({ product }: Props) => {
const SubmitButton = () => {
return (
<FatButton
className="mt-4"
buttonType="button"
type="submit"
text={isPending ? "Saving..." : "Save product"}
Expand All @@ -61,6 +72,19 @@ export const EditProductForm = ({ product }: Props) => {
);
};

const [deleteConfirmation, setDeleteConfirmation] = useState(false);
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const handleDelete = async (e: any) => {
if (!product) return;
e.stopPropagation();
if (!deleteConfirmation) {
setDeleteConfirmation(true);
} else {
setDeleteConfirmation(false);
await deactivateProduct(product.id);
}
};

return (
<>
<form
Expand All @@ -80,7 +104,7 @@ export const EditProductForm = ({ product }: Props) => {
placeholder="Select a category..."
name="category"
defaultValue={defaultCategory}
choices={["Drink", "Snack", "Other"]}
choices={categoriesFormatted}
/>
</div>
<ImageUpload
Expand All @@ -106,7 +130,19 @@ export const EditProductForm = ({ product }: Props) => {

<input type="hidden" name="id" defaultValue={product?.id} />

<SubmitButton />
<div className="mt-4 flex gap-4">
{product?.id && (
<FatButton
buttonType="button"
type="button"
intent={"secondary"}
RightIcon={HiTrash}
text={deleteConfirmation ? "Click again" : "Delete"}
onClick={(e) => handleDelete(e)}
/>
)}
<SubmitButton />
</div>
</form>
</>
);
Expand Down
Loading

0 comments on commit 373ec0f

Please sign in to comment.