Skip to content

Commit

Permalink
Merge pull request #57 from Prodeko/better-admin-tools
Browse files Browse the repository at this point in the history
  • Loading branch information
ccruzkauppila authored Jan 13, 2025
2 parents 9d59ad1 + baa5f84 commit 6abf25c
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "DepositMethod" ADD VALUE 'ADMIN';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,5 @@ enum DepositMethod {
MANUAL_MOBILEPAY
STRIPE
ACCOUNT_MIGRATION
ADMIN
}
14 changes: 10 additions & 4 deletions src/app/(admin)/admin/edit-products/AdminProductSection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { useRef, useState } from "react";
import { HiX } from "react-icons/hi";
import { HiPlus, HiX } from "react-icons/hi";

import { ClientProduct } from "@/common/types";
import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup";
import { FatButton } from "@/components/ui/Buttons/FatButton";
import { EditProductForm } from "@/components/ui/EditProductForm";
import { Input } from "@/components/ui/Input";
import { ListItem } from "@/components/ui/ListItem";
Expand All @@ -30,9 +31,14 @@ export const AdminProductSection = ({ products }: Props) => {
<span className="flex-none text-neutral-500">
Displaying {filteredProducts.length} of {products.length} products
</span>
<Input
placeholder="Search by name or category..."
onChange={(e) => setProductFilter(e.target.value.toLowerCase())}

<FatButton
buttonType="a"
href="/admin/edit-products/new"
text="New product"
intent="primary"
className="portrait:w-full"
RightIcon={HiPlus}
/>
</div>
<div className="flex flex-col divide-y-2 divide-primary-200 ">
Expand Down
15 changes: 15 additions & 0 deletions src/app/(admin)/admin/edit-products/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { AdminTitle } from "@/components/ui/AdminTitle";
import { EditProductForm } from "@/components/ui/EditProductForm";

const NewProduct = () => {
return (
<div className="flex w-full max-w-screen-lg flex-col gap-8 pb-6 lg:w-[80%]">
<AdminTitle withBackButton title="New product" />
<div className="px-8">
<EditProductForm />
</div>
</div>
);
};

export default NewProduct;
12 changes: 0 additions & 12 deletions src/app/(admin)/admin/newProduct/page.tsx

This file was deleted.

5 changes: 3 additions & 2 deletions src/app/(admin)/admin/superadmin/AdminList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
import toast from "react-hot-toast";
import { HiX } from "react-icons/hi";

import { ClientUser } from "@/common/types";
import { changeUserRole } from "@/server/actions/admin/changeRole";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Role, User } from "@prisma/client";

import { NewAdminDialog } from "./NewAdminDialog";

interface Props {
users: User[];
users: ClientUser[];
}

export const AdminList = ({ users }: Props) => {
const adminUsers = users.filter((user) => user.role === "ADMIN");

const [parent] = useAutoAnimate<HTMLDivElement>({ duration: 200 });

const changeRole = async (user: User, role: Role) => {
const changeRole = async (user: ClientUser, role: Role) => {
try {
await changeUserRole(user.id, role);
toast.success("User role changed successfully");
Expand Down
5 changes: 3 additions & 2 deletions src/app/(admin)/admin/superadmin/NewAdminDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { HiPlus, HiX } from "react-icons/hi";

import { ClientUser } from "@/common/types";
import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup";
import { FatButton } from "@/components/ui/Buttons/FatButton";
import { InputWithLabel } from "@/components/ui/Input";
Expand All @@ -12,7 +13,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { Role, User } from "@prisma/client";

interface Props {
users: User[];
users: ClientUser[];
}

const AddAdminButton = (
Expand All @@ -39,7 +40,7 @@ export const NewAdminDialog = ({ users }: Props) => {

const popupRef = useRef<PopupRefActions>(undefined);

const changeRole = async (user: User, role: Role) => {
const changeRole = async (user: ClientUser, role: Role) => {
try {
await changeUserRole(user.id, role);
toast.success("User role changed successfully");
Expand Down
117 changes: 117 additions & 0 deletions src/app/(admin)/admin/users/ManageUserDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"use client";

import { format } from "date-fns";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { HiPencil, HiPlus, HiX } from "react-icons/hi";

import { ClientUser } from "@/common/types";
import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup";
import { FatButton } from "@/components/ui/Buttons/FatButton";
import { IconButton } from "@/components/ui/Buttons/IconButton";
import { InputWithLabel } from "@/components/ui/Input";
import { adminAddFundsAction } from "@/server/actions/transaction/addFunds";
import { Role, User } from "@prisma/client";

interface Props {
user: ClientUser;
}

const EditUserButton = (
<IconButton buttonType="button" Icon={HiPencil} sizing="xs" />
);

export const ManageUserDialog = ({ user }: Props) => {
const [modifyBalanceAmount, setModifyBalanceAmount] = useState<number | "">(
"",
);

const popupRef = useRef<PopupRefActions>(undefined);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Math.round(parseFloat(e.target.value) * 100) / 100;
if (!Number.isNaN(value)) {
setModifyBalanceAmount(value);
} else {
setModifyBalanceAmount("");
}
};

const addToBalance = async () => {
try {
if (modifyBalanceAmount === "" || modifyBalanceAmount === 0)
throw new Error("Please enter a valid amount");
await adminAddFundsAction(modifyBalanceAmount, user.id);
toast.success("Balance added successfully");
} catch (error) {
toast.error(`Failed to add balance: ${error}`);
}
setModifyBalanceAmount("");
};

const removeFromBalance = async () => {
try {
if (modifyBalanceAmount === "" || modifyBalanceAmount === 0)
throw new Error("Please enter a valid amount");
await adminAddFundsAction(-modifyBalanceAmount, user.id);
toast.success("Balance removed successfully");
} catch (error) {
toast.error(`Failed to remove balance: ${error}`);
}
setModifyBalanceAmount("");
};

return (
<AnimatedPopup ref={popupRef} TriggerComponent={EditUserButton}>
<div className="no-scrollbar flex flex-col gap-6 overflow-hidden rounded-xl bg-neutral-50 px-3 py-6 md:px-12 md:py-12 portrait:max-h-[80vh] portrait:w-[80vw] landscape:max-h-[80vh] landscape:w-[50vw] ">
<div className="flex w-full items-center justify-between">
<p className="text-xl font-bold text-primary-400 md:text-3xl">
Manage user
</p>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<div
className="flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border-2 border-primary-400 bg-primary-50 text-lg text-primary-400 md:h-16 md:w-16 md:border-2 md:text-4xl"
onClick={() => popupRef?.current?.closeContainer()}
>
<HiX />
</div>
</div>
<div className="flex w-full flex-col gap-4 text-lg text-neutral-500 md:text-xl">
<span className="text-xl font-medium text-neutral-700 md:text-2xl">
{user.firstName} {user.lastName}
</span>{" "}
<div className="">Username: {user.userName}</div>
<div className="">Role: {user.role}</div>
<div className="">
Created: {format(user.createdAt, "dd.MM.yyyy")}
</div>
</div>

<div className="flex flex-col gap-4">
<InputWithLabel
labelText="Add or remove from balance"
placeholder="Amount to add/remove"
type="number"
tabIndex={-1}
value={modifyBalanceAmount}
onChange={handleInputChange}
/>
<div className="flex gap-4">
<FatButton
buttonType="button"
intent={"secondary"}
text="Remove"
onClick={() => removeFromBalance()}
/>
<FatButton
buttonType="button"
intent={"primary"}
text="Add"
onClick={() => addToBalance()}
/>
</div>
</div>
</div>
</AnimatedPopup>
);
};
45 changes: 45 additions & 0 deletions src/app/(admin)/admin/users/UserList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { ClientUser } from "@/common/types";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { User } from "@prisma/client";

import { ManageUserDialog } from "./ManageUserDialog";

interface Props {
users: ClientUser[];
}

export const UserList = ({ users }: Props) => {
const [parent] = useAutoAnimate<HTMLDivElement>({ duration: 200 });

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">
<span className="flex-none text-neutral-500">
Displaying {users.length} users
</span>
</div>
<div
ref={parent}
className="flex flex-col divide-y-2 divide-neutral-200 px-5 md:px-12 "
>
{users.map((user) => (
<div
key={user.id}
className="text-md flex w-full items-center justify-between py-5 text-neutral-500 md:py-5 md:text-xl"
>
<div>
<span className="text-xl font-medium text-neutral-700 md:text-2xl">
{user.firstName} {user.lastName}
</span>{" "}
({user.userName})
</div>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<ManageUserDialog user={user} />
</div>
))}
</div>
</section>
);
};
16 changes: 16 additions & 0 deletions src/app/(admin)/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AdminTitle } from "@/components/ui/AdminTitle";
import { getAllUsers } from "@/server/db/queries/account";

import { UserList } from "./UserList";

const ManageUsers = async () => {
const users = await getAllUsers();
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 title="Manage users" />
<UserList users={users} />
</div>
);
};

export default ManageUsers;
2 changes: 2 additions & 0 deletions src/app/(admin)/login/admin/AdminLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const AdminLoginForm = () => {
labelText="Namu ID"
placeholder={"Namu Admin"}
name="userName"
autoCapitalize="none"
spellCheck="false"
required
/>
<InputWithLabel
Expand Down
1 change: 1 addition & 0 deletions src/common/enumTranslations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const translatePrismaDepositMethod = (
STRIPE: "Stripe",
MANUAL_MOBILEPAY: "MobilePay",
ACCOUNT_MIGRATION: "Account Migration",
ADMIN: "Admin",
};
return translations[depositMethod];
};
Expand Down
13 changes: 12 additions & 1 deletion src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";

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

// Basetypes
export const IdParser = z
Expand Down Expand Up @@ -30,6 +30,17 @@ export const ClientProductParser = z.object({ id: IdParser }).extend({

export type ClientProduct = z.infer<typeof ClientProductParser>;

export type ClientUser = Pick<
User,
| "id"
| "userName"
| "firstName"
| "lastName"
| "role"
| "createdAt"
| "updatedAt"
>;

export const CartProductParser = ClientProductParser.extend({
quantity: z
.number()
Expand Down
11 changes: 2 additions & 9 deletions src/components/ui/AdminSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"use client";

import { useState } from "react";
import { FaCrown, FaLock } from "react-icons/fa6";
import {
HiChartBar,
HiChevronLeft,
HiChevronRight,
HiOutlinePlusCircle,
HiShoppingCart,
HiSparkles,
HiUsers,
} from "react-icons/hi";

import { SidebarItem } from "@/components/ui/AdminSidebar/SidebarItem";
Expand All @@ -26,12 +23,8 @@ export const AdminSidebar = ({ superadmin }: Props) => {
Icon={HiShoppingCart}
href="/admin/edit-products"
/>
<SidebarItem
text="New"
Icon={HiOutlinePlusCircle}
href="/admin/newProduct"
/>
<SidebarItem text="Wishes" Icon={HiSparkles} href="/admin/wishes" />
<SidebarItem text="Users" Icon={HiUsers} href="/admin/users" />
<SidebarItem text="Stats" Icon={HiChartBar} href="/admin/statistics" />
{superadmin && (
<SidebarItem
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/AdminTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const AdminTitle = ({
>
{withBackButton && (
<HiArrowLeft
className="text-lg md:text-2xl"
className="cursor-pointer text-lg md:text-2xl"
onClick={() => router.back()}
/>
)}
Expand Down
Loading

0 comments on commit 6abf25c

Please sign in to comment.