diff --git a/apps/admin/src/lib/components/molecules/sidebar/layout-sidebar.svelte b/apps/admin/src/lib/components/molecules/sidebar/layout-sidebar.svelte index 6bf4b82..bbcd225 100644 --- a/apps/admin/src/lib/components/molecules/sidebar/layout-sidebar.svelte +++ b/apps/admin/src/lib/components/molecules/sidebar/layout-sidebar.svelte @@ -3,16 +3,17 @@ import * as Sidebar from "$lib/components/ui/sidebar/index.js"; import type { ComponentProps } from "svelte"; + import Icon from "$lib/components/atoms/icon.svelte"; + import { adminSiteNavMap } from "$lib/core/constants"; + import { sessionUserInfo } from "$lib/stores/session.info"; + import { SettingsIcon } from "@lucide/svelte"; + import ProductIcon from "~icons/carbon/carbon-for-ibm-product"; import SessionIcon from "~icons/carbon/prompt-session"; + import HistoryIcon from "~icons/iconamoon/history-light"; import BillListIcon from "~icons/solar/bill-list-linear"; import PackageIcon from "~icons/solar/box-broken"; import DashboardIcon from "~icons/solar/laptop-minimalistic-broken"; import UsersIcon from "~icons/solar/users-group-two-rounded-broken"; - import HistoryIcon from "~icons/iconamoon/history-light"; - import { adminSiteNavMap } from "$lib/core/constants"; - import Icon from "$lib/components/atoms/icon.svelte"; - import { sessionUserInfo } from "$lib/stores/session.info"; - import { SettingsIcon } from "@lucide/svelte"; const mainLinks = [ { @@ -40,6 +41,12 @@ title: "Orders", url: adminSiteNavMap.orders, }, + + { + icon: ProductIcon, + title: "Products", + url: adminSiteNavMap.products, + }, { icon: UsersIcon, title: "Profile", diff --git a/apps/admin/src/lib/core/constants.ts b/apps/admin/src/lib/core/constants.ts index 9f8f19b..6a966d0 100644 --- a/apps/admin/src/lib/core/constants.ts +++ b/apps/admin/src/lib/core/constants.ts @@ -16,8 +16,10 @@ export const adminSiteNavMap = { history: `${adminBasePath}/sessions/history`, }, data: `${adminBasePath}/data`, + products: `${adminBasePath}/products`, settings: `${adminBasePath}/settings`, profile: `${adminBasePath}/profile`, }; export const PUBLIC_SITE_URL = env.PUBLIC_SITE_URL ?? "https://example.com"; +export const PUBLIC_FRONTEND_URL = env.PUBLIC_FRONTEND_URL ?? "https://eg.io"; diff --git a/apps/admin/src/lib/domains/product/controller.ts b/apps/admin/src/lib/domains/product/controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/admin/src/lib/domains/product/data.ts b/apps/admin/src/lib/domains/product/data.ts new file mode 100644 index 0000000..da3eef7 --- /dev/null +++ b/apps/admin/src/lib/domains/product/data.ts @@ -0,0 +1 @@ +export * from "@pkg/logic/domains/product/data"; diff --git a/apps/admin/src/lib/domains/product/product-list.svelte b/apps/admin/src/lib/domains/product/product-list.svelte deleted file mode 100644 index f04f635..0000000 --- a/apps/admin/src/lib/domains/product/product-list.svelte +++ /dev/null @@ -1,4 +0,0 @@ - - -Show the product list here diff --git a/apps/admin/src/lib/domains/product/product-modals.svelte b/apps/admin/src/lib/domains/product/product-modals.svelte deleted file mode 100644 index fab68d5..0000000 --- a/apps/admin/src/lib/domains/product/product-modals.svelte +++ /dev/null @@ -1 +0,0 @@ -Show the product create and details modals here diff --git a/apps/admin/src/lib/domains/product/product.vm.svelte.ts b/apps/admin/src/lib/domains/product/product.vm.svelte.ts index e69de29..79d160e 100644 --- a/apps/admin/src/lib/domains/product/product.vm.svelte.ts +++ b/apps/admin/src/lib/domains/product/product.vm.svelte.ts @@ -0,0 +1,197 @@ +import { PUBLIC_FRONTEND_URL } from "$lib/core/constants"; +import { trpcApiStore } from "$lib/stores/api"; +import { toast } from "svelte-sonner"; +import { get } from "svelte/store"; +import type { + CreateProductPayload, + ProductModel, + UpdateProductPayload, +} from "./data"; + +export class ProductViewModel { + products = $state([]); + loading = $state(false); + formLoading = $state(false); + + // Selected product for edit/delete operations + selectedProduct = $state(null); + + async fetchProducts() { + const api = get(trpcApiStore); + if (!api) { + return toast.error("API client not initialized"); + } + + this.loading = true; + try { + const result = await api.product.getAllProducts.query(); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + this.products = result.data || []; + return true; + } catch (e) { + console.error(e); + toast.error("Failed to fetch products", { + description: "Please try again later", + }); + return false; + } finally { + this.loading = false; + } + } + + async createProduct(payload: CreateProductPayload) { + const api = get(trpcApiStore); + if (!api) { + return toast.error("API client not initialized"); + } + + this.formLoading = true; + try { + const result = await api.product.createProduct.mutate(payload); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + toast.success("Product created successfully"); + await this.fetchProducts(); + return true; + } catch (e) { + toast.error("Failed to create product", { + description: "Please try again later", + }); + return false; + } finally { + this.formLoading = false; + } + } + + async updateProduct(payload: UpdateProductPayload) { + const api = get(trpcApiStore); + if (!api) { + return toast.error("API client not initialized"); + } + + this.formLoading = true; + try { + const result = await api.product.updateProduct.mutate(payload); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + toast.success("Product updated successfully"); + await this.fetchProducts(); + return true; + } catch (e) { + toast.error("Failed to update product", { + description: "Please try again later", + }); + return false; + } finally { + this.formLoading = false; + this.selectedProduct = null; + } + } + + async deleteProduct(id: number) { + const api = get(trpcApiStore); + if (!api) { + return toast.error("API client not initialized"); + } + + this.formLoading = true; + try { + const result = await api.product.deleteProduct.mutate({ id }); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return false; + } + + toast.success("Product deleted successfully"); + await this.fetchProducts(); + return true; + } catch (e) { + toast.error("Failed to delete product", { + description: "Please try again later", + }); + return false; + } finally { + this.formLoading = false; + this.selectedProduct = null; + } + } + + async refreshProductLinkId(id: number) { + const api = get(trpcApiStore); + if (!api) { + return toast.error("API client not initialized"); + } + + try { + const result = await api.product.refreshProductLinkId.mutate({ id }); + if (result.error) { + toast.error(result.error.message, { + description: result.error.userHint, + }); + return null; + } + + toast.success("Link ID refreshed successfully"); + await this.fetchProducts(); + return result.data; + } catch (e) { + toast.error("Failed to refresh link ID", { + description: "Please try again later", + }); + return null; + } + } + + selectProduct(product: ProductModel) { + this.selectedProduct = { ...product }; + } + + clearSelectedProduct() { + this.selectedProduct = null; + } + + getDefaultProduct(): CreateProductPayload { + return { + title: "", + description: "", + longDescription: "", + price: 0, + discountPrice: 0, + }; + } + + async copyLinkToClipboard(linkId: string) { + try { + await navigator.clipboard.writeText( + `${PUBLIC_FRONTEND_URL}/${linkId}`, + ); + toast.success("Frontend link copied to clipboard"); + return true; + } catch (e) { + toast.error("Failed to copy link ID", { + description: "Please try copying manually", + }); + return false; + } + } +} + +export const productVM = new ProductViewModel(); diff --git a/apps/admin/src/lib/domains/product/repository.ts b/apps/admin/src/lib/domains/product/repository.ts new file mode 100644 index 0000000..78a069e --- /dev/null +++ b/apps/admin/src/lib/domains/product/repository.ts @@ -0,0 +1 @@ +export * from "@pkg/logic/domains/product/repository"; diff --git a/apps/admin/src/lib/domains/product/router.ts b/apps/admin/src/lib/domains/product/router.ts index da5a990..ee360d1 100644 --- a/apps/admin/src/lib/domains/product/router.ts +++ b/apps/admin/src/lib/domains/product/router.ts @@ -1,9 +1,47 @@ import { protectedProcedure } from "$lib/server/trpc/t"; import { createTRPCRouter } from "$lib/trpc/t"; +import { z } from "zod"; +import { createProductPayload, updateProductPayload } from "./data"; +import { getProductUseCases } from "./usecases"; export const productRouter = createTRPCRouter({ getAllProducts: protectedProcedure.query(async ({}) => { - return {}; + const controller = getProductUseCases(); + return controller.getAllProducts(); }), - // TODO: complete the implementation of this + + getProductById: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input }) => { + const controller = getProductUseCases(); + return controller.getProductById(input.id); + }), + + createProduct: protectedProcedure + .input(createProductPayload) + .mutation(async ({ input }) => { + const controller = getProductUseCases(); + return controller.createProduct(input); + }), + + updateProduct: protectedProcedure + .input(updateProductPayload) + .mutation(async ({ input }) => { + const controller = getProductUseCases(); + return controller.updateProduct(input); + }), + + deleteProduct: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const controller = getProductUseCases(); + return controller.deleteProduct(input.id); + }), + + refreshProductLinkId: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + const controller = getProductUseCases(); + return controller.refreshProductLinkId(input.id); + }), }); diff --git a/apps/admin/src/lib/domains/product/usecases.ts b/apps/admin/src/lib/domains/product/usecases.ts new file mode 100644 index 0000000..7c72fba --- /dev/null +++ b/apps/admin/src/lib/domains/product/usecases.ts @@ -0,0 +1 @@ +export * from "@pkg/logic/domains/product/usecases"; diff --git a/apps/admin/src/lib/domains/product/view/product-form.svelte b/apps/admin/src/lib/domains/product/view/product-form.svelte new file mode 100644 index 0000000..baefcd7 --- /dev/null +++ b/apps/admin/src/lib/domains/product/view/product-form.svelte @@ -0,0 +1,111 @@ + + +
+ +
+ + +
+ + +
+ +