robodev 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/emit-starter.test.ts +36 -0
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/api/_lib.ts +9 -2
- package/templates/backend/package.json +1 -1
- package/templates/catalog.json +5 -0
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/.config/local.spa.template.yml +27 -0
- package/templates/marketplace/.oxlint-base.json +29 -0
- package/templates/marketplace/.oxlintrc.json +78 -0
- package/templates/marketplace/.rulesync/rules/frontend-api-boundary.md +36 -0
- package/templates/marketplace/.rulesync/rules/frontend-component-structure.md +38 -0
- package/templates/marketplace/.rulesync/rules/frontend-forms.md +32 -0
- package/templates/marketplace/.rulesync/rules/frontend-notifications.md +24 -0
- package/templates/marketplace/.rulesync/rules/frontend-povio-components.md +42 -0
- package/templates/marketplace/.rulesync/rules/frontend-povio-ui.md +45 -0
- package/templates/marketplace/.rulesync/rules/frontend-query-autocomplete.md +29 -0
- package/templates/marketplace/.rulesync/rules/frontend-tables-and-lists.md +34 -0
- package/templates/marketplace/.rulesync/rules/frontend-translations.md +26 -0
- package/templates/marketplace/.rulesync/rules/project-overview.md +32 -0
- package/templates/marketplace/.rulesync/rules/robodev-api.md +30 -0
- package/templates/marketplace/.rulesync/rules/robodev-auth.md +40 -0
- package/templates/marketplace/.rulesync/rules/robodev-database.md +22 -0
- package/templates/marketplace/.rulesync/rules/role-based-app-structure.md +36 -0
- package/templates/marketplace/.rulesync/skills/media-feature/SKILL.md +21 -0
- package/templates/marketplace/.rulesync/skills/povio-ui-styling/SKILL.md +208 -0
- package/templates/marketplace/.rulesync/skills/povio-ui-styling/agents/openai.yaml +4 -0
- package/templates/marketplace/.rulesync/skills/robodev-api-route/SKILL.md +13 -0
- package/templates/marketplace/.rulesync/skills/robodev-table/SKILL.md +12 -0
- package/templates/marketplace/README.md +40 -0
- package/templates/marketplace/api/_lib.ts +677 -0
- package/templates/marketplace/api/cart/[id].ts +51 -0
- package/templates/marketplace/api/cart.ts +70 -0
- package/templates/marketplace/api/categories.ts +11 -0
- package/templates/marketplace/api/checkout.ts +157 -0
- package/templates/marketplace/api/health.ts +7 -0
- package/templates/marketplace/api/listings/[id].ts +101 -0
- package/templates/marketplace/api/listings/paginate.ts +29 -0
- package/templates/marketplace/api/listings.ts +90 -0
- package/templates/marketplace/api/me.ts +14 -0
- package/templates/marketplace/api/orders/[id]/complete.ts +27 -0
- package/templates/marketplace/api/orders/[id]/ship.ts +24 -0
- package/templates/marketplace/api/orders/[id].ts +15 -0
- package/templates/marketplace/api/purchases.ts +19 -0
- package/templates/marketplace/api/reviews.ts +58 -0
- package/templates/marketplace/api/sales.ts +19 -0
- package/templates/marketplace/api/watches/[listingId].ts +18 -0
- package/templates/marketplace/api/watches.ts +51 -0
- package/templates/marketplace/apps/fe/index.html +24 -0
- package/templates/marketplace/apps/fe/openapi-codegen.config.ts +72 -0
- package/templates/marketplace/apps/fe/package.json +84 -0
- package/templates/marketplace/apps/fe/public/apple-touch-icon.png +0 -0
- package/templates/marketplace/apps/fe/public/favicon-96x96.png +0 -0
- package/templates/marketplace/apps/fe/public/favicon.ico +0 -0
- package/templates/marketplace/apps/fe/public/favicon.svg +3 -0
- package/templates/marketplace/apps/fe/public/site.webmanifest +21 -0
- package/templates/marketplace/apps/fe/public/web-app-manifest-192x192.png +0 -0
- package/templates/marketplace/apps/fe/public/web-app-manifest-512x512.png +0 -0
- package/templates/marketplace/apps/fe/src/assets/fonts/GeneralSans-Bold.otf +0 -0
- package/templates/marketplace/apps/fe/src/assets/fonts/GeneralSans-Medium.otf +0 -0
- package/templates/marketplace/apps/fe/src/assets/fonts/GeneralSans-Regular.otf +0 -0
- package/templates/marketplace/apps/fe/src/assets/fonts/GeneralSans-Semibold.otf +0 -0
- package/templates/marketplace/apps/fe/src/assets/locales/en/translation.json +335 -0
- package/templates/marketplace/apps/fe/src/assets/locales/sl/translation.json +335 -0
- package/templates/marketplace/apps/fe/src/clients/app-rest-client.ts +15 -0
- package/templates/marketplace/apps/fe/src/clients/auth-token-store.ts +101 -0
- package/templates/marketplace/apps/fe/src/clients/rest/app-error-handler.ts +31 -0
- package/templates/marketplace/apps/fe/src/clients/rest/interceptors/authorization-header.interceptor.ts +15 -0
- package/templates/marketplace/apps/fe/src/clients/rest/interceptors/refresh-token.interceptor.ts +37 -0
- package/templates/marketplace/apps/fe/src/clients/rest/interceptors/response.interceptor.ts +17 -0
- package/templates/marketplace/apps/fe/src/components/404.tsx +18 -0
- package/templates/marketplace/apps/fe/src/components/features/auth/AuthBrandPanel.tsx +60 -0
- package/templates/marketplace/apps/fe/src/components/features/auth/AuthLayout.tsx +23 -0
- package/templates/marketplace/apps/fe/src/components/features/auth/LoginPage.tsx +111 -0
- package/templates/marketplace/apps/fe/src/components/features/auth/RegisterPage.tsx +143 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/BrowsePage.tsx +76 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/CartPage.tsx +88 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/CheckoutPage.tsx +193 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingCard.tsx +50 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingDetailsPage.tsx +152 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/ListingFormPage.tsx +243 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/MyListingsPage.tsx +84 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/OrdersPage.tsx +224 -0
- package/templates/marketplace/apps/fe/src/components/features/marketplace/WatchlistPage.tsx +56 -0
- package/templates/marketplace/apps/fe/src/components/features/profile/ProfilePage.tsx +96 -0
- package/templates/marketplace/apps/fe/src/components/googleAnalytics/GoogleAnalytics.tsx +35 -0
- package/templates/marketplace/apps/fe/src/components/layout/AppLayout.tsx +19 -0
- package/templates/marketplace/apps/fe/src/components/layout/app-header/AppHeader.tsx +95 -0
- package/templates/marketplace/apps/fe/src/components/layout/app-header/MobileNavigation.tsx +55 -0
- package/templates/marketplace/apps/fe/src/components/layout/app-header/NavLink.tsx +29 -0
- package/templates/marketplace/apps/fe/src/components/shared/branding/BrandLogo.tsx +13 -0
- package/templates/marketplace/apps/fe/src/components/shared/error/ErrorFallback.tsx +43 -0
- package/templates/marketplace/apps/fe/src/components/shared/error/ErrorText.tsx +20 -0
- package/templates/marketplace/apps/fe/src/components/shared/error/NotFound.tsx +36 -0
- package/templates/marketplace/apps/fe/src/components/shared/forms/RequiredLabel.tsx +21 -0
- package/templates/marketplace/apps/fe/src/components/shared/forms/RowInputWrapper.tsx +50 -0
- package/templates/marketplace/apps/fe/src/components/shared/head/AppHead.tsx +32 -0
- package/templates/marketplace/apps/fe/src/components/shared/head/DefaultAppHead.tsx +34 -0
- package/templates/marketplace/apps/fe/src/components/shared/layout/LoadingState.tsx +9 -0
- package/templates/marketplace/apps/fe/src/components/shared/layout/ThinPageWrapper.tsx +5 -0
- package/templates/marketplace/apps/fe/src/components/shared/page/PageHeader.tsx +69 -0
- package/templates/marketplace/apps/fe/src/components/shared/ui/Card.tsx +60 -0
- package/templates/marketplace/apps/fe/src/components/shared/ui/GoogleLoginButton.tsx +19 -0
- package/templates/marketplace/apps/fe/src/components/shared/ui/RequiredLabel.tsx +22 -0
- package/templates/marketplace/apps/fe/src/components/shared/ui/TableActions.tsx +22 -0
- package/templates/marketplace/apps/fe/src/config/app.config.ts +35 -0
- package/templates/marketplace/apps/fe/src/config/i18n.ts +43 -0
- package/templates/marketplace/apps/fe/src/config/inits/a11y.ts +14 -0
- package/templates/marketplace/apps/fe/src/config/inits/logger.ts +7 -0
- package/templates/marketplace/apps/fe/src/config/inits/sentry.ts +21 -0
- package/templates/marketplace/apps/fe/src/config/jwt.config.ts +2 -0
- package/templates/marketplace/apps/fe/src/config/query.config.ts +20 -0
- package/templates/marketplace/apps/fe/src/hooks/useAuth.ts +5 -0
- package/templates/marketplace/apps/fe/src/main.tsx +52 -0
- package/templates/marketplace/apps/fe/src/pages/(guest)/login.tsx +23 -0
- package/templates/marketplace/apps/fe/src/pages/(guest)/register.tsx +23 -0
- package/templates/marketplace/apps/fe/src/pages/(guest)/route.tsx +18 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/cart.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/checkout.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/index.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/listings/$id/index.tsx +37 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/my-listings.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/orders/$id.tsx +40 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/orders/index.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/profile.tsx +23 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/route.tsx +18 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/sales.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/sell/$id.tsx +44 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/sell/index.tsx +19 -0
- package/templates/marketplace/apps/fe/src/pages/(private)/watchlist.tsx +22 -0
- package/templates/marketplace/apps/fe/src/pages/(public)/auth.tsx +37 -0
- package/templates/marketplace/apps/fe/src/pages/(public)/route.tsx +9 -0
- package/templates/marketplace/apps/fe/src/pages/__root.tsx +164 -0
- package/templates/marketplace/apps/fe/src/providers/AppErrorBoundary.tsx +10 -0
- package/templates/marketplace/apps/fe/src/providers/OpenApiRuntimeProvider.tsx +31 -0
- package/templates/marketplace/apps/fe/src/providers/index.tsx +44 -0
- package/templates/marketplace/apps/fe/src/providers/jwt.provider.tsx +95 -0
- package/templates/marketplace/apps/fe/src/routeTree.gen.ts +435 -0
- package/templates/marketplace/apps/fe/src/styles/base.css +103 -0
- package/templates/marketplace/apps/fe/src/styles/fonts/fonts.tsx +10 -0
- package/templates/marketplace/apps/fe/src/styles/fonts/general-sans.css +31 -0
- package/templates/marketplace/apps/fe/src/styles/globals.css +28 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/button.override.ts +536 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/checkbox.override.ts +71 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/input.override.ts +252 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/label.override.ts +91 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/modal.override.ts +57 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/radio.override.ts +46 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/table.override.ts +104 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/tag.override.ts +66 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/defaults/typography.override.ts +115 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/outline.clsx.ts +10 -0
- package/templates/marketplace/apps/fe/src/styles/overrides/uiOverrides.override.ts +60 -0
- package/templates/marketplace/apps/fe/src/styles/theme.css +2177 -0
- package/templates/marketplace/apps/fe/src/types/i18next.d.ts +12 -0
- package/templates/marketplace/apps/fe/src/types/table.d.ts +17 -0
- package/templates/marketplace/apps/fe/src/types/ui.d.ts +26 -0
- package/templates/marketplace/apps/fe/src/types/vite-env.d.ts +20 -0
- package/templates/marketplace/apps/fe/src/utils/date.utils.ts +17 -0
- package/templates/marketplace/apps/fe/src/utils/image-fallback.ts +9 -0
- package/templates/marketplace/apps/fe/src/utils/listing-form.test.ts +69 -0
- package/templates/marketplace/apps/fe/src/utils/listing-form.ts +77 -0
- package/templates/marketplace/apps/fe/src/utils/listing-submit.ts +29 -0
- package/templates/marketplace/apps/fe/src/utils/number.utils.ts +12 -0
- package/templates/marketplace/apps/fe/src/utils/string.utils.ts +5 -0
- package/templates/marketplace/apps/fe/src/vite-env.d.ts +1 -0
- package/templates/marketplace/apps/fe/tsconfig.app.json +32 -0
- package/templates/marketplace/apps/fe/tsconfig.json +9 -0
- package/templates/marketplace/apps/fe/tsconfig.node.json +31 -0
- package/templates/marketplace/apps/fe/vite.config.ts +125 -0
- package/templates/marketplace/database.ts +154 -0
- package/templates/marketplace/openapi.json +2303 -0
- package/templates/marketplace/oxfmt.config.js +23 -0
- package/templates/marketplace/package.json +18 -0
- package/templates/marketplace/rulesync.jsonc +18 -0
- package/templates/marketplace/tsconfig.json +11 -0
- package/templates/space/api/_lib.ts +9 -2
- package/templates/space/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "i18next";
|
|
2
|
+
|
|
3
|
+
import type { defaultNS, resources } from "@/config/i18n";
|
|
4
|
+
|
|
5
|
+
declare module "i18next" {
|
|
6
|
+
interface CustomTypeOptions {
|
|
7
|
+
enableSelector: true;
|
|
8
|
+
returnNull: false;
|
|
9
|
+
defaultNS: typeof defaultNS;
|
|
10
|
+
resources: (typeof resources)["en"];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* oxlint-disable unused-imports/no-unused-vars */
|
|
2
|
+
import "@tanstack/react-table";
|
|
3
|
+
|
|
4
|
+
declare module "@tanstack/react-table" {
|
|
5
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
6
|
+
width?: string;
|
|
7
|
+
headerClass?: string;
|
|
8
|
+
cellClass?: string;
|
|
9
|
+
sortClass?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CellContext<TData extends RowData, TValue> {
|
|
13
|
+
original: TData;
|
|
14
|
+
data: TData;
|
|
15
|
+
value: TValue;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "@povio/ui";
|
|
2
|
+
import type { UIOverrides } from "@povio/ui";
|
|
3
|
+
import type { LinkProps } from "@tanstack/react-router";
|
|
4
|
+
import type { RowData } from "@tanstack/react-table";
|
|
5
|
+
|
|
6
|
+
import type { tableRowOverride } from "@/styles/overrides/defaults/table.override";
|
|
7
|
+
import type { typographyOverride } from "@/styles/overrides/defaults/typography.override";
|
|
8
|
+
|
|
9
|
+
declare module "@povio/ui" {
|
|
10
|
+
interface LinkNavigationProps extends LinkProps {}
|
|
11
|
+
|
|
12
|
+
interface TypographyVariantProps {
|
|
13
|
+
size?: UIOverrides.VariantProps<typeof typographyOverride>["size"];
|
|
14
|
+
sizeMobile?: UIOverrides.VariantProps<typeof typographyOverride>["sizeMobile"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TableRowVariantProps {
|
|
18
|
+
rowSize?: UIOverrides.VariantProps<typeof tableRowOverride>["rowSize"];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "@tanstack/react-table" {
|
|
23
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
24
|
+
filterKey?: string;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_PUBLIC_ENABLE_WEB_VITALS: "true" | "false";
|
|
5
|
+
readonly VITE_PUBLIC_API_URL: string;
|
|
6
|
+
readonly VITE_PUBLIC_API_MODE?: "fake" | "real";
|
|
7
|
+
readonly VITE_PUBLIC_FAKE_BE_SEED_VERSION?: string;
|
|
8
|
+
readonly VITE_PUBLIC_LOG_LEVEL: "error" | "warn";
|
|
9
|
+
readonly VITE_PUBLIC_SENTRY_DSN: string;
|
|
10
|
+
readonly VITE_PUBLIC_SENTRY_ENVIRONMENT: string;
|
|
11
|
+
readonly VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: string;
|
|
12
|
+
readonly VITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: string;
|
|
13
|
+
readonly VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: string;
|
|
14
|
+
readonly VITE_PUBLIC_GOOGLE_ANALYTICS_MEASUREMENT_ID: string;
|
|
15
|
+
readonly VITE_PUBLIC_AUTH_CUSTOM_JWT_ENABLE_MAGIC_LINK: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ImportMeta {
|
|
19
|
+
readonly env: ImportMetaEnv;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DateUtils as PovioDateUtils } from "@povio/ui";
|
|
2
|
+
|
|
3
|
+
export namespace DateUtils {
|
|
4
|
+
export const parseDate = (date: string | Date): Date => (typeof date === "string" ? new Date(date) : date);
|
|
5
|
+
|
|
6
|
+
export const formatDate = (date: string | Date, format = "dd/MM/yyyy") => {
|
|
7
|
+
const parsedDate = parseDate(date);
|
|
8
|
+
|
|
9
|
+
return PovioDateUtils.formatDate(parsedDate, format);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const formatDateTime = (date: string | Date, format = "dd/MM/yyyy HH:mm") => {
|
|
13
|
+
const parsedDate = parseDate(date);
|
|
14
|
+
|
|
15
|
+
return PovioDateUtils.formatDate(parsedDate, format);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface ImageFallbackOptions {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
text?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getFallbackImageUrl({ width, height, text = "No image" }: ImageFallbackOptions): string {
|
|
8
|
+
return `https://placehold.co/${width}x${height}?text=${encodeURIComponent(text)}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
bindLocalListingImages,
|
|
5
|
+
buildListingFormData,
|
|
6
|
+
dollarsToCents,
|
|
7
|
+
listingFormRequestConfig,
|
|
8
|
+
localListingImageUpload,
|
|
9
|
+
} from "./listing-form";
|
|
10
|
+
|
|
11
|
+
describe("listing multipart submit", () => {
|
|
12
|
+
it("builds FormData with text fields and multiple files", () => {
|
|
13
|
+
const first = new File(["png"], "one.png", { type: "image/png" });
|
|
14
|
+
const second = new File(["jpg"], "two.jpg", { type: "image/jpeg" });
|
|
15
|
+
const form = buildListingFormData({
|
|
16
|
+
title: "Lamp",
|
|
17
|
+
description: "Desk lamp",
|
|
18
|
+
categoryId: "cat-1",
|
|
19
|
+
priceCents: 2500,
|
|
20
|
+
quantity: 2,
|
|
21
|
+
shippingCents: 400,
|
|
22
|
+
shippingDays: 3,
|
|
23
|
+
files: [first, second],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(form.get("title")).toBe("Lamp");
|
|
27
|
+
expect(form.get("priceCents")).toBe("2500");
|
|
28
|
+
expect(form.getAll("files")).toHaveLength(2);
|
|
29
|
+
expect(form.get("clearImage")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("sends removeImageIds when updating without those media ids", () => {
|
|
33
|
+
const form = buildListingFormData({
|
|
34
|
+
title: "Lamp",
|
|
35
|
+
categoryId: "cat-1",
|
|
36
|
+
priceCents: 2500,
|
|
37
|
+
quantity: 1,
|
|
38
|
+
shippingCents: 400,
|
|
39
|
+
shippingDays: 3,
|
|
40
|
+
removeImageIds: ["media-1", "media-2"],
|
|
41
|
+
});
|
|
42
|
+
expect(form.get("removeImageIds")).toBe("media-1,media-2");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not set Content-Type so the browser can add the multipart boundary", () => {
|
|
46
|
+
const config = listingFormRequestConfig() as { headers?: Record<string, string> };
|
|
47
|
+
expect(config.headers?.["Content-Type"]).toBeUndefined();
|
|
48
|
+
expect("Content-Type" in (config.headers ?? {})).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("local FileUpload handler never fetches", async () => {
|
|
52
|
+
const file = new File(["png"], "lamp.png", { type: "image/png" });
|
|
53
|
+
const result = await localListingImageUpload({ data: {} } as never, file);
|
|
54
|
+
expect(result).toEqual({ id: "lamp.png" });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("bindLocalListingImages keeps Files locally without fetching", async () => {
|
|
58
|
+
const seen: File[] = [];
|
|
59
|
+
const handlers = bindLocalListingImages((file) => seen.push(file));
|
|
60
|
+
const file = new File(["png"], "lamp.png", { type: "image/png" });
|
|
61
|
+
await handlers.fileUpload({ data: {} }, file);
|
|
62
|
+
expect(seen).toEqual([file]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("converts dollars to integer cents", () => {
|
|
66
|
+
expect(dollarsToCents("12.99")).toBe(1299);
|
|
67
|
+
expect(dollarsToCents(12)).toBe(1200);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type ListingWriteFields = {
|
|
2
|
+
title: string;
|
|
3
|
+
description?: string | null;
|
|
4
|
+
categoryId: string;
|
|
5
|
+
priceCents: number;
|
|
6
|
+
quantity: number;
|
|
7
|
+
shippingCents: number;
|
|
8
|
+
shippingDays: number;
|
|
9
|
+
status?: string;
|
|
10
|
+
files?: File[];
|
|
11
|
+
removeImageIds?: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function appendOptional(form: FormData, key: string, value: unknown) {
|
|
15
|
+
if (value == null || value === "") return;
|
|
16
|
+
form.append(key, String(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildListingFormData(fields: ListingWriteFields): FormData {
|
|
20
|
+
const form = new FormData();
|
|
21
|
+
form.append("title", fields.title);
|
|
22
|
+
appendOptional(form, "description", fields.description);
|
|
23
|
+
form.append("categoryId", fields.categoryId);
|
|
24
|
+
form.append("priceCents", String(fields.priceCents));
|
|
25
|
+
form.append("quantity", String(fields.quantity));
|
|
26
|
+
form.append("shippingCents", String(fields.shippingCents));
|
|
27
|
+
form.append("shippingDays", String(fields.shippingDays));
|
|
28
|
+
appendOptional(form, "status", fields.status);
|
|
29
|
+
for (const file of fields.files ?? []) {
|
|
30
|
+
form.append("files", file);
|
|
31
|
+
form.append("file", file);
|
|
32
|
+
}
|
|
33
|
+
if (fields.removeImageIds?.length) {
|
|
34
|
+
form.append("removeImageIds", fields.removeImageIds.join(","));
|
|
35
|
+
}
|
|
36
|
+
return form;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Axios/browser must set the multipart boundary. Never set Content-Type. */
|
|
40
|
+
export function listingFormRequestConfig() {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function localListingImageUpload(
|
|
45
|
+
_request: unknown,
|
|
46
|
+
file: File,
|
|
47
|
+
): Promise<{ id: string }> {
|
|
48
|
+
return { id: file.name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function bindLocalListingImages(onFile: (file: File) => void, onClear?: () => void) {
|
|
52
|
+
return {
|
|
53
|
+
fileUpload: async (request: unknown, file: File) => {
|
|
54
|
+
onFile(file);
|
|
55
|
+
return localListingImageUpload(request, file);
|
|
56
|
+
},
|
|
57
|
+
fileRemove: async () => {
|
|
58
|
+
onClear?.();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function dollarsToCents(value: string | number): number {
|
|
64
|
+
const amount = typeof value === "number" ? value : Number(value);
|
|
65
|
+
if (!Number.isFinite(amount)) return 0;
|
|
66
|
+
return Math.round(amount * 100);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function centsToDollarsInput(cents: number): string {
|
|
70
|
+
return (cents / 100).toFixed(2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatCents(cents: number): string {
|
|
74
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency: "USD" }).format(
|
|
75
|
+
cents / 100,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AppRestClient } from "@/clients/app-rest-client";
|
|
2
|
+
import { ListingModels } from "@/openapi/listing/listing.models";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
buildListingFormData,
|
|
6
|
+
listingFormRequestConfig,
|
|
7
|
+
type ListingWriteFields,
|
|
8
|
+
} from "./listing-form";
|
|
9
|
+
|
|
10
|
+
export async function createListing(fields: ListingWriteFields): Promise<ListingModels.ListingDto> {
|
|
11
|
+
return AppRestClient.post(
|
|
12
|
+
{ resSchema: ListingModels.ListingDtoSchema },
|
|
13
|
+
"/api/listings",
|
|
14
|
+
buildListingFormData(fields),
|
|
15
|
+
listingFormRequestConfig(),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function updateListing(
|
|
20
|
+
id: string,
|
|
21
|
+
fields: ListingWriteFields,
|
|
22
|
+
): Promise<ListingModels.ListingDto> {
|
|
23
|
+
return AppRestClient.put(
|
|
24
|
+
{ resSchema: ListingModels.ListingDtoSchema },
|
|
25
|
+
`/api/listings/${id}`,
|
|
26
|
+
buildListingFormData(fields),
|
|
27
|
+
listingFormRequestConfig(),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export namespace NumberUtils {
|
|
2
|
+
export function formatNumber(value: number, options?: Intl.NumberFormatOptions): string {
|
|
3
|
+
return new Intl.NumberFormat(undefined, options).format(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function formatInteger(value: number, options?: Intl.NumberFormatOptions): string {
|
|
7
|
+
return formatNumber(value, {
|
|
8
|
+
maximumFractionDigits: 0,
|
|
9
|
+
...options,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client", "node"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": false,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Absolute imports */
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/* Linting */
|
|
25
|
+
"strict": true,
|
|
26
|
+
"noUnusedLocals": true,
|
|
27
|
+
"noUnusedParameters": true,
|
|
28
|
+
"noFallthroughCasesInSwitch": true,
|
|
29
|
+
"noUncheckedSideEffectImports": true
|
|
30
|
+
},
|
|
31
|
+
"include": ["src", "node_modules/@povio/ui/dist/index.d.ts"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Absolute imports */
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["./src/*"]
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/* Linting */
|
|
23
|
+
"strict": true,
|
|
24
|
+
"noUnusedLocals": true,
|
|
25
|
+
"noUnusedParameters": true,
|
|
26
|
+
"erasableSyntaxOnly": true,
|
|
27
|
+
"noFallthroughCasesInSwitch": true,
|
|
28
|
+
"noUncheckedSideEffectImports": true
|
|
29
|
+
},
|
|
30
|
+
"include": ["vite.config.ts"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { applyEnv, resolveConfigSync, getNumber, getString } from "@povio/resolve-config";
|
|
2
|
+
import reactScan from "@povio/vite-plugin-react-scan";
|
|
3
|
+
import tailwindCSS from "@tailwindcss/vite";
|
|
4
|
+
import { devtools } from "@tanstack/devtools-vite";
|
|
5
|
+
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
|
6
|
+
import viteReact from "@vitejs/plugin-react";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { defineConfig as defineViteConfig, mergeConfig } from "vite";
|
|
10
|
+
import devtoolsJson from "vite-plugin-devtools-json";
|
|
11
|
+
import { defineConfig as defineVitestConfig } from "vitest/config";
|
|
12
|
+
import { reactIconsSprite } from "react-icons-sprite/vite";
|
|
13
|
+
|
|
14
|
+
const repoRoot = new URL("../..", import.meta.url).pathname;
|
|
15
|
+
|
|
16
|
+
function readDotEnvKey(root: string, key: string): string | undefined {
|
|
17
|
+
try {
|
|
18
|
+
const content = readFileSync(join(root, ".env"), "utf8");
|
|
19
|
+
const line = content.split(/\r?\n/).find((row) => row.startsWith(`${key}=`));
|
|
20
|
+
const value = line?.slice(key.length + 1).trim();
|
|
21
|
+
return value || undefined;
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadResolvedSpaEnv(cwd: string): Record<string, unknown> {
|
|
28
|
+
try {
|
|
29
|
+
const resolved = resolveConfigSync({ cwd, module: "spa", target: "resolved" });
|
|
30
|
+
if (resolved && typeof resolved === "object") {
|
|
31
|
+
return resolved as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// unnamed or missing spa manifest
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const env = loadResolvedSpaEnv(repoRoot);
|
|
40
|
+
applyEnv(env, "__");
|
|
41
|
+
|
|
42
|
+
const apiUrl =
|
|
43
|
+
getString(env, "VITE_PUBLIC_API_URL") ||
|
|
44
|
+
process.env.VITE_PUBLIC_API_URL ||
|
|
45
|
+
readDotEnvKey(repoRoot, "VITE_PUBLIC_API_URL") ||
|
|
46
|
+
"http://localhost:4000";
|
|
47
|
+
|
|
48
|
+
const isInTestMode = process.env.VITEST === "true";
|
|
49
|
+
const iconsSpritePlugin = reactIconsSprite();
|
|
50
|
+
const iconsSpriteTransform = iconsSpritePlugin.transform;
|
|
51
|
+
|
|
52
|
+
if (typeof iconsSpriteTransform === "function") {
|
|
53
|
+
iconsSpritePlugin.transform = function transformAppIcons(code, id, ...args) {
|
|
54
|
+
if (id.includes("/node_modules/")) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return iconsSpriteTransform.call(this, code, id, ...args);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const createViteConfig = (isDevServer: boolean) => ({
|
|
63
|
+
plugins: [
|
|
64
|
+
isDevServer && reactScan(),
|
|
65
|
+
!isInTestMode &&
|
|
66
|
+
tanstackRouter({
|
|
67
|
+
target: "react",
|
|
68
|
+
autoCodeSplitting: true,
|
|
69
|
+
routesDirectory: "./src/pages",
|
|
70
|
+
}),
|
|
71
|
+
devtools({
|
|
72
|
+
eventBusConfig: { enabled: false },
|
|
73
|
+
}),
|
|
74
|
+
iconsSpritePlugin,
|
|
75
|
+
viteReact({
|
|
76
|
+
babel: {
|
|
77
|
+
plugins: ["babel-plugin-react-compiler"],
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
tailwindCSS(),
|
|
81
|
+
!isInTestMode && devtoolsJson(),
|
|
82
|
+
],
|
|
83
|
+
server: {
|
|
84
|
+
open: false,
|
|
85
|
+
port: getNumber(env, "VITE_DEV_PORT") ?? 3000,
|
|
86
|
+
strictPort: false,
|
|
87
|
+
proxy: {
|
|
88
|
+
"^/api(?:/|$)": {
|
|
89
|
+
target: apiUrl,
|
|
90
|
+
changeOrigin: true,
|
|
91
|
+
secure: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
build: {
|
|
96
|
+
target: "esnext",
|
|
97
|
+
},
|
|
98
|
+
optimizeDeps: {
|
|
99
|
+
entries: ["src/**/*.{ts,tsx}"],
|
|
100
|
+
},
|
|
101
|
+
worker: {
|
|
102
|
+
format: "es",
|
|
103
|
+
},
|
|
104
|
+
resolve: {
|
|
105
|
+
alias: {
|
|
106
|
+
"@": new URL("src", import.meta.url).pathname,
|
|
107
|
+
},
|
|
108
|
+
dedupe: ["react", "react-dom", "i18next", "react-i18next"],
|
|
109
|
+
tsconfigPaths: true,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const vitestConfig = defineVitestConfig({
|
|
114
|
+
test: {
|
|
115
|
+
root: "./",
|
|
116
|
+
include: ["src/**/*.test.{ts,tsx}"],
|
|
117
|
+
watch: false,
|
|
118
|
+
globals: true,
|
|
119
|
+
reporters: ["default"],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default defineViteConfig(({ command }) =>
|
|
124
|
+
mergeConfig(createViteConfig(command === "serve"), vitestConfig),
|
|
125
|
+
);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineDatabase,
|
|
3
|
+
integer,
|
|
4
|
+
pgTable,
|
|
5
|
+
text,
|
|
6
|
+
timestamp,
|
|
7
|
+
unique,
|
|
8
|
+
uniqueIndex,
|
|
9
|
+
uuid,
|
|
10
|
+
} from "@robodev-ai/sdk";
|
|
11
|
+
|
|
12
|
+
export const media = pgTable("media", {
|
|
13
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
14
|
+
key: text("key").notNull(),
|
|
15
|
+
userId: text("userId").notNull(),
|
|
16
|
+
resourceName: text("resourceName").notNull(),
|
|
17
|
+
fileName: text("fileName").notNull(),
|
|
18
|
+
fileSize: integer("fileSize").notNull(),
|
|
19
|
+
mimeType: text("mimeType").notNull(),
|
|
20
|
+
uploaded: timestamp("uploaded", { withTimezone: true }),
|
|
21
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const categories = pgTable(
|
|
25
|
+
"categories",
|
|
26
|
+
{
|
|
27
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
28
|
+
slug: text("slug").notNull(),
|
|
29
|
+
name: text("name").notNull(),
|
|
30
|
+
},
|
|
31
|
+
(table) => [uniqueIndex("categories_slug_idx").on(table.slug)],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const listings = pgTable("listings", {
|
|
35
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
36
|
+
sellerId: text("sellerId").notNull(),
|
|
37
|
+
sellerName: text("sellerName"),
|
|
38
|
+
sellerEmail: text("sellerEmail").notNull(),
|
|
39
|
+
categoryId: uuid("categoryId")
|
|
40
|
+
.notNull()
|
|
41
|
+
.references(() => categories.id),
|
|
42
|
+
title: text("title").notNull(),
|
|
43
|
+
description: text("description"),
|
|
44
|
+
priceCents: integer("priceCents").notNull(),
|
|
45
|
+
quantity: integer("quantity").notNull(),
|
|
46
|
+
shippingCents: integer("shippingCents").notNull(),
|
|
47
|
+
shippingDays: integer("shippingDays").notNull(),
|
|
48
|
+
status: text("status").notNull().default("active"),
|
|
49
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
50
|
+
updatedAt: timestamp("updatedAt", { withTimezone: true }).defaultNow(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const listingImages = pgTable("listing_images", {
|
|
54
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
55
|
+
listingId: uuid("listingId")
|
|
56
|
+
.notNull()
|
|
57
|
+
.references(() => listings.id, { onDelete: "cascade" }),
|
|
58
|
+
mediaId: uuid("mediaId")
|
|
59
|
+
.notNull()
|
|
60
|
+
.references(() => media.id, { onDelete: "cascade" }),
|
|
61
|
+
sortOrder: integer("sortOrder").notNull().default(0),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const cartItems = pgTable(
|
|
65
|
+
"cart_items",
|
|
66
|
+
{
|
|
67
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
68
|
+
userId: text("userId").notNull(),
|
|
69
|
+
listingId: uuid("listingId")
|
|
70
|
+
.notNull()
|
|
71
|
+
.references(() => listings.id, { onDelete: "cascade" }),
|
|
72
|
+
quantity: integer("quantity").notNull(),
|
|
73
|
+
},
|
|
74
|
+
(table) => [unique("cart_items_user_listing").on(table.userId, table.listingId)],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const orders = pgTable("orders", {
|
|
78
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
79
|
+
buyerId: text("buyerId").notNull(),
|
|
80
|
+
buyerEmail: text("buyerEmail").notNull(),
|
|
81
|
+
sellerId: text("sellerId").notNull(),
|
|
82
|
+
sellerEmail: text("sellerEmail").notNull(),
|
|
83
|
+
status: text("status").notNull(),
|
|
84
|
+
shippingName: text("shippingName").notNull(),
|
|
85
|
+
shippingLine1: text("shippingLine1").notNull(),
|
|
86
|
+
shippingLine2: text("shippingLine2"),
|
|
87
|
+
shippingCity: text("shippingCity").notNull(),
|
|
88
|
+
shippingRegion: text("shippingRegion"),
|
|
89
|
+
shippingPostal: text("shippingPostal").notNull(),
|
|
90
|
+
shippingCountry: text("shippingCountry").notNull(),
|
|
91
|
+
shippingCents: integer("shippingCents").notNull(),
|
|
92
|
+
totalCents: integer("totalCents").notNull(),
|
|
93
|
+
last4: text("last4"),
|
|
94
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
95
|
+
updatedAt: timestamp("updatedAt", { withTimezone: true }).defaultNow(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const orderItems = pgTable("order_items", {
|
|
99
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
100
|
+
orderId: uuid("orderId")
|
|
101
|
+
.notNull()
|
|
102
|
+
.references(() => orders.id, { onDelete: "cascade" }),
|
|
103
|
+
listingId: uuid("listingId")
|
|
104
|
+
.notNull()
|
|
105
|
+
.references(() => listings.id),
|
|
106
|
+
title: text("title").notNull(),
|
|
107
|
+
qty: integer("qty").notNull(),
|
|
108
|
+
unitPriceCents: integer("unitPriceCents").notNull(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const watches = pgTable(
|
|
112
|
+
"watches",
|
|
113
|
+
{
|
|
114
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
115
|
+
userId: text("userId").notNull(),
|
|
116
|
+
listingId: uuid("listingId")
|
|
117
|
+
.notNull()
|
|
118
|
+
.references(() => listings.id, { onDelete: "cascade" }),
|
|
119
|
+
},
|
|
120
|
+
(table) => [unique("watches_user_listing").on(table.userId, table.listingId)],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
export const reviews = pgTable(
|
|
124
|
+
"reviews",
|
|
125
|
+
{
|
|
126
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
127
|
+
listingId: uuid("listingId")
|
|
128
|
+
.notNull()
|
|
129
|
+
.references(() => listings.id, { onDelete: "cascade" }),
|
|
130
|
+
orderId: uuid("orderId")
|
|
131
|
+
.notNull()
|
|
132
|
+
.references(() => orders.id, { onDelete: "cascade" }),
|
|
133
|
+
buyerId: text("buyerId").notNull(),
|
|
134
|
+
rating: integer("rating").notNull(),
|
|
135
|
+
body: text("body"),
|
|
136
|
+
createdAt: timestamp("createdAt", { withTimezone: true }).defaultNow(),
|
|
137
|
+
},
|
|
138
|
+
(table) => [unique("reviews_order_listing").on(table.orderId, table.listingId)],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export default defineDatabase({
|
|
142
|
+
name: "marketplace",
|
|
143
|
+
tables: {
|
|
144
|
+
media,
|
|
145
|
+
categories,
|
|
146
|
+
listings,
|
|
147
|
+
listingImages,
|
|
148
|
+
cartItems,
|
|
149
|
+
orders,
|
|
150
|
+
orderItems,
|
|
151
|
+
watches,
|
|
152
|
+
reviews,
|
|
153
|
+
},
|
|
154
|
+
});
|