Compare commits
5 Commits
9cee6b0671
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c1ab930e48 | |||
| 9013905d01 | |||
| 23efcab80c | |||
| 2f5537cf7e | |||
| 51663df27d |
32
.gitea/workflows/ci.yml
Normal file
32
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: Lint, Typecheck & Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: npm run type-check
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm run test:once
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,7 +30,6 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Convex
|
# Convex
|
||||||
convex/_generated
|
|
||||||
apps/admin/.env.staging
|
apps/admin/.env.staging
|
||||||
apps/storefront/.env.staging
|
apps/storefront/.env.staging
|
||||||
convex/.env.staging
|
convex/.env.staging
|
||||||
|
|||||||
12
apps/admin/eslint.config.mjs
Normal file
12
apps/admin/eslint.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const compat = new FlatCompat({ baseDirectory: __dirname });
|
||||||
|
|
||||||
|
const config = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
{ ignores: [".next/**", "node_modules/**"] },
|
||||||
|
];
|
||||||
|
export default config;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-require-imports -- Next.js config commonly uses require */
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev --port 3001",
|
"dev": "next dev --port 3001",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint .",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { useQuery } from "convex/react"
|
|||||||
import { api } from "../../../../../../convex/_generated/api"
|
import { api } from "../../../../../../convex/_generated/api"
|
||||||
import type { Id } from "../../../../../../convex/_generated/dataModel"
|
import type { Id } from "../../../../../../convex/_generated/dataModel"
|
||||||
import { ProductSearchSection } from "../../../components/shared/ProductSearchSection"
|
import { ProductSearchSection } from "../../../components/shared/ProductSearchSection"
|
||||||
import { ProductImageCarousel } from "../../../components/images/ProductImageCarousel"
|
import {
|
||||||
|
ProductImageCarousel,
|
||||||
|
type ProductImage,
|
||||||
|
} from "../../../components/images/ProductImageCarousel"
|
||||||
import { ImageUploadSection } from "../../../components/images/ImageUploadSection"
|
import { ImageUploadSection } from "../../../components/images/ImageUploadSection"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
@@ -79,7 +82,7 @@ export default function ImagesPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ProductImageCarousel
|
<ProductImageCarousel
|
||||||
images={images as any}
|
images={images as ProductImage[]}
|
||||||
onAddMore={() => setShowUpload(true)}
|
onAddMore={() => setShowUpload(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export default function EditProductPage() {
|
|||||||
categoryId: payload.categoryId as Id<"categories">,
|
categoryId: payload.categoryId as Id<"categories">,
|
||||||
})
|
})
|
||||||
router.push("/products")
|
router.push("/products")
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e?.message ?? "Failed to save product. Please try again.")
|
setError(e instanceof Error ? e.message : "Failed to save product. Please try again.")
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +64,8 @@ export default function EditProductPage() {
|
|||||||
try {
|
try {
|
||||||
await archiveProduct({ id: productId })
|
await archiveProduct({ id: productId })
|
||||||
router.push("/products")
|
router.push("/products")
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e?.message ?? "Failed to archive product.")
|
setError(e instanceof Error ? e.message : "Failed to archive product.")
|
||||||
setIsArchiving(false)
|
setIsArchiving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export default function NewProductPage() {
|
|||||||
categoryId: payload.categoryId as Id<"categories">,
|
categoryId: payload.categoryId as Id<"categories">,
|
||||||
})
|
})
|
||||||
router.push("/products")
|
router.push("/products")
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
setError(e?.message ?? "Failed to create product. Please try again.")
|
setError(e instanceof Error ? e.message : "Failed to create product. Please try again.")
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,11 +199,14 @@ export default function ProductsPage() {
|
|||||||
? searchResults === undefined
|
? searchResults === undefined
|
||||||
: listStatus === "LoadingFirstPage"
|
: listStatus === "LoadingFirstPage"
|
||||||
|
|
||||||
const rawProducts = isSearching ? (searchResults ?? []) : listResults
|
const rawProducts = useMemo(
|
||||||
|
() => (isSearching ? (searchResults ?? []) : listResults),
|
||||||
|
[isSearching, searchResults, listResults],
|
||||||
|
)
|
||||||
|
|
||||||
const products = useMemo(() => {
|
const products = useMemo(() => {
|
||||||
if (!sortField) return rawProducts
|
if (!sortField) return rawProducts
|
||||||
return [...rawProducts].sort((a: any, b: any) => {
|
return [...rawProducts].sort((a: PreviewProduct, b: PreviewProduct) => {
|
||||||
const aVal: string =
|
const aVal: string =
|
||||||
sortField === "name"
|
sortField === "name"
|
||||||
? (a.name ?? "")
|
? (a.name ?? "")
|
||||||
@@ -235,8 +238,8 @@ export default function ProductsPage() {
|
|||||||
setVisibleCols((prev) => ({ ...prev, [key]: checked }))
|
setVisibleCols((prev) => ({ ...prev, [key]: checked }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPreview(product: any) {
|
function openPreview(product: PreviewProduct) {
|
||||||
setPreviewProduct(product as PreviewProduct)
|
setPreviewProduct(product)
|
||||||
setPreviewOpen(true)
|
setPreviewOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +377,7 @@ export default function ProductsPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
products.map((product: any) => {
|
products.map((product: PreviewProduct) => {
|
||||||
const statusCfg =
|
const statusCfg =
|
||||||
STATUS_CONFIG[product.status as keyof typeof STATUS_CONFIG]
|
STATUS_CONFIG[product.status as keyof typeof STATUS_CONFIG]
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { DM_Sans, Geist, Geist_Mono } from "next/font/google";
|
import { DM_Sans } from "next/font/google";
|
||||||
import { ClerkProvider } from "@clerk/nextjs";
|
import { ClerkProvider } from "@clerk/nextjs";
|
||||||
import { ConvexClientProvider } from "@repo/convex";
|
import { ConvexClientProvider } from "@repo/convex";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const dmSans = DM_Sans({subsets:['latin'],variable:'--font-sans'});
|
const dmSans = DM_Sans({ subsets: ["latin"], variable: "--font-sans" });
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef } from "react"
|
import { useState, useRef } from "react"
|
||||||
|
import Image from "next/image"
|
||||||
import { useMutation } from "convex/react"
|
import { useMutation } from "convex/react"
|
||||||
import { api } from "../../../../../convex/_generated/api"
|
import { api } from "../../../../../convex/_generated/api"
|
||||||
import type { Id } from "../../../../../convex/_generated/dataModel"
|
import type { Id } from "../../../../../convex/_generated/dataModel"
|
||||||
@@ -61,8 +62,8 @@ export function ImageUploadSection({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errJson = await res.json().catch(() => ({}))
|
const errJson = (await res.json().catch(() => ({}))) as { detail?: string }
|
||||||
throw new Error((errJson as any).detail ?? "Background removal failed")
|
throw new Error(errJson.detail ?? "Background removal failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await res.blob()
|
const blob = await res.blob()
|
||||||
@@ -100,8 +101,8 @@ export function ImageUploadSection({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!uploadRes.ok) {
|
if (!uploadRes.ok) {
|
||||||
const errJson = await uploadRes.json().catch(() => ({}))
|
const errJson = (await uploadRes.json().catch(() => ({}))) as { error?: string }
|
||||||
throw new Error((errJson as any).error ?? "Upload failed")
|
throw new Error(errJson.error ?? "Upload failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url } = await uploadRes.json()
|
const { url } = await uploadRes.json()
|
||||||
@@ -188,8 +189,14 @@ export function ImageUploadSection({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Original</p>
|
<p className="text-xs font-medium text-muted-foreground">Original</p>
|
||||||
{localUrl && (
|
{localUrl && (
|
||||||
<div className="aspect-square overflow-hidden rounded-md border bg-muted">
|
<div className="relative aspect-square overflow-hidden rounded-md border bg-muted">
|
||||||
<img src={localUrl} alt="Original" className="h-full w-full object-contain" />
|
<Image
|
||||||
|
src={localUrl}
|
||||||
|
alt="Original"
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -199,11 +206,13 @@ export function ImageUploadSection({
|
|||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<Skeleton className="aspect-square w-full rounded-md" />
|
<Skeleton className="aspect-square w-full rounded-md" />
|
||||||
) : processedUrl ? (
|
) : processedUrl ? (
|
||||||
<div className="aspect-square overflow-hidden rounded-md border bg-[conic-gradient(#e5e7eb_25%,_#fff_25%,_#fff_50%,_#e5e7eb_50%,_#e5e7eb_75%,_#fff_75%)] bg-[length:16px_16px]">
|
<div className="relative aspect-square overflow-hidden rounded-md border bg-[conic-gradient(#e5e7eb_25%,_#fff_25%,_#fff_50%,_#e5e7eb_50%,_#e5e7eb_75%,_#fff_75%)] bg-[length:16px_16px]">
|
||||||
<img
|
<Image
|
||||||
src={processedUrl}
|
src={processedUrl}
|
||||||
alt="Background removed"
|
alt="Background removed"
|
||||||
className="h-full w-full object-contain"
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : processingError ? (
|
) : processingError ? (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
|
import Image from "next/image"
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -35,7 +36,7 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
import { Delete02Icon, ImageAdd01Icon, DragDropVerticalIcon } from "@hugeicons/core-free-icons"
|
import { Delete02Icon, ImageAdd01Icon, DragDropVerticalIcon } from "@hugeicons/core-free-icons"
|
||||||
|
|
||||||
interface ProductImage {
|
export interface ProductImage {
|
||||||
_id: Id<"productImages">
|
_id: Id<"productImages">
|
||||||
url: string
|
url: string
|
||||||
alt?: string
|
alt?: string
|
||||||
@@ -105,11 +106,12 @@ function SortableImageCard({
|
|||||||
|
|
||||||
{/* Row 2 in DOM → Row 2 visually (middle stays middle) */}
|
{/* Row 2 in DOM → Row 2 visually (middle stays middle) */}
|
||||||
{/* Image — appears in CENTER */}
|
{/* Image — appears in CENTER */}
|
||||||
<div className="w-28 aspect-square rotate-180 overflow-hidden rounded-md border bg-muted">
|
<div className="relative w-28 aspect-square rotate-180 overflow-hidden rounded-md border bg-muted">
|
||||||
<img
|
<Image
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.alt ?? "Product image"}
|
alt={image.alt ?? "Product image"}
|
||||||
className="h-full w-full object-contain"
|
fill
|
||||||
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function ProductSearchSection({ onSelect, onClear, selectedId }: ProductS
|
|||||||
|
|
||||||
{!isLoading && results && results.length > 0 && (
|
{!isLoading && results && results.length > 0 && (
|
||||||
<ul className="max-w-sm divide-y rounded-md border">
|
<ul className="max-w-sm divide-y rounded-md border">
|
||||||
{results.map((product: any) => (
|
{results.map((product: { _id: string; name: string }) => (
|
||||||
<li key={product._id}>
|
<li key={product._id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { HugeiconsIcon } from "@hugeicons/react";
|
|
||||||
import { Store01Icon } from "@hugeicons/core-free-icons";
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export function AcceptReturnButton({ orderId }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
toast.error((result as { success: false; code: string; message: string }).message)
|
toast.error((result as { success: false; code: string; message: string }).message)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to accept return.")
|
toast.error(e instanceof Error ? e.message : "Failed to accept return.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export function CreateLabelButton({ orderId }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
toast.error((result as { success: false; code: string; message: string }).message)
|
toast.error((result as { success: false; code: string; message: string }).message)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to create shipping label.")
|
toast.error(e instanceof Error ? e.message : "Failed to create shipping label.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export function IssueRefundButton({ orderId, total, currency }: Props) {
|
|||||||
await issueRefund({ orderId })
|
await issueRefund({ orderId })
|
||||||
toast.success("Refund issued.")
|
toast.success("Refund issued.")
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to issue refund.")
|
toast.error(e instanceof Error ? e.message : "Failed to issue refund.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export function MarkReturnReceivedButton({ orderId }: Props) {
|
|||||||
await markReceived({ id: orderId })
|
await markReceived({ id: orderId })
|
||||||
toast.success("Return marked as received.")
|
toast.success("Return marked as received.")
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to mark return as received.")
|
toast.error(e instanceof Error ? e.message : "Failed to mark return as received.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export function UpdateStatusDialog({ orderId, currentStatus }: Props) {
|
|||||||
const label = ORDER_STATUS_CONFIG[selectedStatus as OrderStatus]?.label
|
const label = ORDER_STATUS_CONFIG[selectedStatus as OrderStatus]?.label
|
||||||
toast.success(`Status updated to ${label ?? selectedStatus}`)
|
toast.success(`Status updated to ${label ?? selectedStatus}`)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to update status.")
|
toast.error(e instanceof Error ? e.message : "Failed to update status.")
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export function ProductSearchSection({ onSelect, onClear, selectedId }: ProductS
|
|||||||
|
|
||||||
{!isLoading && results && results.length > 0 && (
|
{!isLoading && results && results.length > 0 && (
|
||||||
<ul className="max-w-sm divide-y rounded-md border">
|
<ul className="max-w-sm divide-y rounded-md border">
|
||||||
{results.map((product: any) => (
|
{results.map((product: { _id: string; name: string }) => (
|
||||||
<li key={product._id}>
|
<li key={product._id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ export function CreateVariantDialog({
|
|||||||
|
|
||||||
toast.success(`Variant "${values.name}" created`)
|
toast.success(`Variant "${values.name}" created`)
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to create variant")
|
toast.error(e instanceof Error ? e.message : "Failed to create variant")
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export function EditVariantDialog({
|
|||||||
|
|
||||||
toast.success(`"${values.name}" updated`)
|
toast.success(`"${values.name}" updated`)
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to update variant")
|
toast.error(e instanceof Error ? e.message : "Failed to update variant")
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ function VariantActionsMenu({
|
|||||||
try {
|
try {
|
||||||
await updateVariant({ id: variant._id, isActive: !variant.isActive })
|
await updateVariant({ id: variant._id, isActive: !variant.isActive })
|
||||||
toast.success(`"${variant.name}" ${variant.isActive ? "deactivated" : "activated"}`)
|
toast.success(`"${variant.name}" ${variant.isActive ? "deactivated" : "activated"}`)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to update variant")
|
toast.error(e instanceof Error ? e.message : "Failed to update variant")
|
||||||
} finally {
|
} finally {
|
||||||
setIsToggling(false)
|
setIsToggling(false)
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,8 @@ function VariantActionsMenu({
|
|||||||
await deleteVariant({ id: variant._id })
|
await deleteVariant({ id: variant._id })
|
||||||
toast.success(`"${variant.name}" deleted`)
|
toast.success(`"${variant.name}" deleted`)
|
||||||
setDeleteOpen(false)
|
setDeleteOpen(false)
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
toast.error(e?.message ?? "Failed to delete variant")
|
toast.error(e instanceof Error ? e.message : "Failed to delete variant")
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false)
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
|
|||||||
12
apps/storefront/eslint.config.mjs
Normal file
12
apps/storefront/eslint.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const compat = new FlatCompat({ baseDirectory: __dirname });
|
||||||
|
|
||||||
|
const config = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
{ ignores: [".next/**", "node_modules/**"] },
|
||||||
|
];
|
||||||
|
export default config;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-require-imports -- Next.js config commonly uses require */
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev --port 3000",
|
"dev": "next dev --port 3000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint .",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Chip, RadioGroup, Radio, Label, Skeleton } from "@heroui/react";
|
import { Button, Chip, RadioGroup, Radio, Skeleton } from "@heroui/react";
|
||||||
import type { CheckoutAddress } from "@/lib/checkout/types";
|
import type { CheckoutAddress } from "@/lib/checkout/types";
|
||||||
|
|
||||||
type AddressSelectorProps = {
|
type AddressSelectorProps = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useConvexAuth } from "convex/react";
|
import { useConvexAuth } from "convex/react";
|
||||||
import { UserButton } from "@clerk/nextjs";
|
import { UserButton } from "@clerk/nextjs";
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export function HeaderUserAction() {
|
|||||||
|
|
||||||
if (isLoading || !isAuthenticated) {
|
if (isLoading || !isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<a
|
<Link
|
||||||
href="/sign-in"
|
href="/sign-in"
|
||||||
className="group flex flex-col items-center gap-1"
|
className="group flex flex-col items-center gap-1"
|
||||||
>
|
>
|
||||||
@@ -48,7 +49,7 @@ export function HeaderUserAction() {
|
|||||||
<span className="text-[10px] font-medium tracking-wide text-[#3d5554] transition-colors group-hover:text-[#236f6b]">
|
<span className="text-[10px] font-medium tracking-wide text-[#3d5554] transition-colors group-hover:text-[#236f6b]">
|
||||||
Sign In
|
Sign In
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { useConvexAuth } from "convex/react";
|
import { useConvexAuth } from "convex/react";
|
||||||
import { UserButton } from "@clerk/nextjs";
|
import { UserButton } from "@clerk/nextjs";
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export function MobileHeaderUserAction() {
|
|||||||
|
|
||||||
if (isLoading || !isAuthenticated) {
|
if (isLoading || !isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<a
|
<Link
|
||||||
href="/sign-in"
|
href="/sign-in"
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e8e7] bg-[#f9fcfb]"
|
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#d9e8e7] bg-[#f9fcfb]"
|
||||||
>
|
>
|
||||||
@@ -45,7 +46,7 @@ export function MobileHeaderUserAction() {
|
|||||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="12" cy="7" r="4" />
|
<circle cx="12" cy="7" r="4" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import { useQuery } from "convex/react";
|
import { useQuery } from "convex/react";
|
||||||
import { api } from "../../../../../../convex/_generated/api";
|
import { api } from "../../../../../../convex/_generated/api";
|
||||||
import type { Id } from "../../../../../../convex/_generated/dataModel";
|
import type { Id } from "../../../../../../convex/_generated/dataModel";
|
||||||
import { ReviewSortOption } from "@/lib/product-detail/types";
|
import type { ProductDetailReview, ReviewSortOption } from "@/lib/product-detail/types";
|
||||||
import { ReviewSortBar } from "./ReviewSortBar";
|
import { ReviewSortBar } from "./ReviewSortBar";
|
||||||
import { ReviewList } from "./ReviewList";
|
import { ReviewList } from "./ReviewList";
|
||||||
import { ReviewForm } from "./ReviewForm";
|
import { ReviewForm } from "./ReviewForm";
|
||||||
@@ -58,7 +58,7 @@ export function ProductDetailReviewsPanel({ productId, initialRating, initialRev
|
|||||||
onSortChange={handleSortChange}
|
onSortChange={handleSortChange}
|
||||||
/>
|
/>
|
||||||
<ReviewList
|
<ReviewList
|
||||||
reviews={page as any}
|
reviews={page as ProductDetailReview[]}
|
||||||
total={total}
|
total={total}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
isLoading={result === undefined}
|
isLoading={result === undefined}
|
||||||
|
|||||||
@@ -206,7 +206,10 @@ export function ProductDetailHeroSection({
|
|||||||
const [addError, setAddError] = useState<string | null>(null);
|
const [addError, setAddError] = useState<string | null>(null);
|
||||||
|
|
||||||
const images: ProductDetailImage[] = product.images ?? [];
|
const images: ProductDetailImage[] = product.images ?? [];
|
||||||
const variants: ProductDetailVariant[] = product.variants ?? [];
|
const variants = useMemo(
|
||||||
|
() => (product.variants ?? []) as ProductDetailVariant[],
|
||||||
|
[product.variants],
|
||||||
|
);
|
||||||
const mainImage = images[selectedImageIndex] ?? images[0];
|
const mainImage = images[selectedImageIndex] ?? images[0];
|
||||||
const dimensions = useMemo(
|
const dimensions = useMemo(
|
||||||
() => getAttributeDimensions(variants),
|
() => getAttributeDimensions(variants),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function ProductDetailHeroSectionWrapper({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductDetailHeroSection
|
<ProductDetailHeroSection
|
||||||
product={product as any}
|
product={product}
|
||||||
category={category}
|
category={category}
|
||||||
subCategory={subCategory}
|
subCategory={subCategory}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { fetchQuery } from "convex/nextjs";
|
import { fetchQuery } from "convex/nextjs";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { api } from "../../../../../../convex/_generated/api";
|
import { api } from "../../../../../../convex/_generated/api";
|
||||||
|
|||||||
@@ -22,16 +22,6 @@ type ShopFilterContentProps = {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILTER_SECTION_IDS = [
|
|
||||||
"brand",
|
|
||||||
"tags",
|
|
||||||
"petSize",
|
|
||||||
"ageRange",
|
|
||||||
"specialDiet",
|
|
||||||
"material",
|
|
||||||
"flavor",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function ShopFilterContent({
|
export function ShopFilterContent({
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { ShopFilterState } from "@/lib/shop/filterState";
|
|||||||
* otherwise renders children (e.g. sub-category links).
|
* otherwise renders children (e.g. sub-category links).
|
||||||
*/
|
*/
|
||||||
export function ShopFilterSidebar({
|
export function ShopFilterSidebar({
|
||||||
children,
|
|
||||||
className,
|
className,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
filterState,
|
filterState,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export function ShopToolbar({
|
|||||||
currentSort,
|
currentSort,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
onOpenFilter,
|
onOpenFilter,
|
||||||
resultCount,
|
|
||||||
}: {
|
}: {
|
||||||
sortOptions: SortOption[];
|
sortOptions: SortOption[];
|
||||||
currentSort: string;
|
currentSort: string;
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { WishlistSignInPrompt } from "./state/WishlistSignInPrompt";
|
|||||||
export function WishlistPageView() {
|
export function WishlistPageView() {
|
||||||
const { isAuthenticated, isLoading: authLoading } = useConvexAuth();
|
const { isAuthenticated, isLoading: authLoading } = useConvexAuth();
|
||||||
const { items, isLoading, isEmpty } = useWishlist();
|
const { items, isLoading, isEmpty } = useWishlist();
|
||||||
const { removeItem, isRemoving, addToCart, isAddingToCart } =
|
const { removeItem, isRemoving, addToCart } = useWishlistMutations();
|
||||||
useWishlistMutations();
|
|
||||||
|
|
||||||
const [removeTarget, setRemoveTarget] = useState<WishlistItem | null>(null);
|
const [removeTarget, setRemoveTarget] = useState<WishlistItem | null>(null);
|
||||||
const [removingId, setRemovingId] = useState<string | null>(null);
|
const [removingId, setRemovingId] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WISHLIST_PATH } from "@/lib/wishlist/constants";
|
|
||||||
|
|
||||||
function HeartIcon() {
|
function HeartIcon() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useMutation } from "convex/react";
|
import { useMutation } from "convex/react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { api } from "../../../../../convex/_generated/api";
|
import { api } from "../../../../../convex/_generated/api";
|
||||||
|
import type { Id } from "../../../../../convex/_generated/dataModel";
|
||||||
import type { AddressFormData } from "./types";
|
import type { AddressFormData } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,7 @@ export function useAddressMutations(): {
|
|||||||
data: Partial<AddressFormData> & { isValidated?: boolean },
|
data: Partial<AddressFormData> & { isValidated?: boolean },
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await updateMutation({
|
await updateMutation({
|
||||||
id: id as any,
|
id: id as Id<"addresses">,
|
||||||
...(data.firstName !== undefined && { firstName: data.firstName }),
|
...(data.firstName !== undefined && { firstName: data.firstName }),
|
||||||
...(data.lastName !== undefined && { lastName: data.lastName }),
|
...(data.lastName !== undefined && { lastName: data.lastName }),
|
||||||
...(data.phone !== undefined && { phone: data.phone }),
|
...(data.phone !== undefined && { phone: data.phone }),
|
||||||
@@ -69,14 +70,14 @@ export function useAddressMutations(): {
|
|||||||
|
|
||||||
const setDefault = useCallback(
|
const setDefault = useCallback(
|
||||||
async (id: string): Promise<void> => {
|
async (id: string): Promise<void> => {
|
||||||
await setDefaultMutation({ id: id as any });
|
await setDefaultMutation({ id: id as Id<"addresses"> });
|
||||||
},
|
},
|
||||||
[setDefaultMutation],
|
[setDefaultMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const markValidated = useCallback(
|
const markValidated = useCallback(
|
||||||
async (id: string, isValidated: boolean): Promise<void> => {
|
async (id: string, isValidated: boolean): Promise<void> => {
|
||||||
await markValidatedMutation({ id: id as any, isValidated });
|
await markValidatedMutation({ id: id as Id<"addresses">, isValidated });
|
||||||
},
|
},
|
||||||
[markValidatedMutation],
|
[markValidatedMutation],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ describe("useShippingRate", () => {
|
|||||||
|
|
||||||
expect(result.current.result).toBeNull();
|
expect(result.current.result).toBeNull();
|
||||||
expect(result.current.error).toBe(
|
expect(result.current.error).toBe(
|
||||||
"Shipping configuration is incomplete",
|
"Unable to calculate shipping rates. Please try again.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +140,9 @@ describe("useShippingRate", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.error).toBe("Network timeout");
|
expect(result.current.error).toBe(
|
||||||
|
"Unable to calculate shipping rates. Please try again.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
mockActionFn.mockResolvedValue(sampleResult);
|
mockActionFn.mockResolvedValue(sampleResult);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe("useClickOutside", () => {
|
|||||||
|
|
||||||
it("does not call handler when enabled is false", () => {
|
it("does not call handler when enabled is false", () => {
|
||||||
const handler = vi.fn();
|
const handler = vi.fn();
|
||||||
const { result } = renderHook(() => {
|
renderHook(() => {
|
||||||
const ref = useRef<HTMLDivElement>(document.createElement("div"));
|
const ref = useRef<HTMLDivElement>(document.createElement("div"));
|
||||||
useClickOutside([ref], handler, false);
|
useClickOutside([ref], handler, false);
|
||||||
return ref;
|
return ref;
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
import { renderHook } from "@testing-library/react";
|
import { renderHook } from "@testing-library/react";
|
||||||
import { useCartSession } from "./useCartSession";
|
import { useCartSession } from "./useCartSession";
|
||||||
|
|
||||||
const mockUseAuth = vi.fn();
|
const mockUseConvexAuth = vi.fn();
|
||||||
vi.mock("@clerk/nextjs", () => ({ useAuth: () => mockUseAuth() }));
|
vi.mock("convex/react", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("convex/react")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useConvexAuth: () => mockUseConvexAuth(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const mockGetGuestSessionId = vi.fn();
|
const mockGetGuestSessionId = vi.fn();
|
||||||
const mockSetGuestSessionCookie = vi.fn();
|
const mockSetGuestSessionCookie = vi.fn();
|
||||||
@@ -21,17 +27,18 @@ describe("useCartSession", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockGenerateGuestSessionId.mockReturnValue("generated-uuid-123");
|
mockGenerateGuestSessionId.mockReturnValue("generated-uuid-123");
|
||||||
|
mockUseConvexAuth.mockReturnValue({ isLoading: false, isAuthenticated: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns authenticated session when loaded and signed in", () => {
|
it("returns authenticated session when loaded and signed in", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: true, isSignedIn: true });
|
mockUseConvexAuth.mockReturnValue({ isLoading: false, isAuthenticated: true });
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current).toEqual({ sessionId: undefined, isGuest: false });
|
expect(result.current).toEqual({ sessionId: undefined, isGuest: false });
|
||||||
expect(mockGetGuestSessionId).not.toHaveBeenCalled();
|
expect(mockGetGuestSessionId).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns guest session with cookie value when not signed in and cookie present", () => {
|
it("returns guest session with cookie value when not signed in and cookie present", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: true, isSignedIn: false });
|
mockUseConvexAuth.mockReturnValue({ isLoading: false, isAuthenticated: false });
|
||||||
mockGetGuestSessionId.mockReturnValue("cookie-uuid-456");
|
mockGetGuestSessionId.mockReturnValue("cookie-uuid-456");
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
@@ -44,7 +51,7 @@ describe("useCartSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns guest session and sets cookie when not signed in and cookie missing", () => {
|
it("returns guest session and sets cookie when not signed in and cookie missing", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: true, isSignedIn: false });
|
mockUseConvexAuth.mockReturnValue({ isLoading: false, isAuthenticated: false });
|
||||||
mockGetGuestSessionId.mockReturnValue(null);
|
mockGetGuestSessionId.mockReturnValue(null);
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
@@ -57,7 +64,7 @@ describe("useCartSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("treats not-loaded auth as guest and uses cookie when present", () => {
|
it("treats not-loaded auth as guest and uses cookie when present", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: false, isSignedIn: false });
|
mockUseConvexAuth.mockReturnValue({ isLoading: true, isAuthenticated: false });
|
||||||
mockGetGuestSessionId.mockReturnValue("existing-guest-id");
|
mockGetGuestSessionId.mockReturnValue("existing-guest-id");
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
@@ -68,7 +75,7 @@ describe("useCartSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("treats not-loaded auth as guest and creates session when cookie missing", () => {
|
it("treats not-loaded auth as guest and creates session when cookie missing", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: false, isSignedIn: false });
|
mockUseConvexAuth.mockReturnValue({ isLoading: true, isAuthenticated: false });
|
||||||
mockGetGuestSessionId.mockReturnValue(null);
|
mockGetGuestSessionId.mockReturnValue(null);
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
@@ -79,7 +86,7 @@ describe("useCartSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns authenticated only when both isLoaded and isSignedIn are true", () => {
|
it("returns authenticated only when both isLoaded and isSignedIn are true", () => {
|
||||||
mockUseAuth.mockReturnValue({ isLoaded: true, isSignedIn: false });
|
mockUseConvexAuth.mockReturnValue({ isLoading: false, isAuthenticated: false });
|
||||||
mockGetGuestSessionId.mockReturnValue("guest-id");
|
mockGetGuestSessionId.mockReturnValue("guest-id");
|
||||||
const { result } = renderHook(() => useCartSession());
|
const { result } = renderHook(() => useCartSession());
|
||||||
expect(result.current.isGuest).toBe(true);
|
expect(result.current.isGuest).toBe(true);
|
||||||
|
|||||||
247
convex/_generated/api.d.ts
vendored
Normal file
247
convex/_generated/api.d.ts
vendored
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type * as addresses from "../addresses.js";
|
||||||
|
import type * as adminInvitations from "../adminInvitations.js";
|
||||||
|
import type * as carts from "../carts.js";
|
||||||
|
import type * as categories from "../categories.js";
|
||||||
|
import type * as checkout from "../checkout.js";
|
||||||
|
import type * as checkoutActions from "../checkoutActions.js";
|
||||||
|
import type * as emails from "../emails.js";
|
||||||
|
import type * as fulfillmentActions from "../fulfillmentActions.js";
|
||||||
|
import type * as http from "../http.js";
|
||||||
|
import type * as model_carts from "../model/carts.js";
|
||||||
|
import type * as model_categories from "../model/categories.js";
|
||||||
|
import type * as model_checkout from "../model/checkout.js";
|
||||||
|
import type * as model_orders from "../model/orders.js";
|
||||||
|
import type * as model_products from "../model/products.js";
|
||||||
|
import type * as model_shippo from "../model/shippo.js";
|
||||||
|
import type * as model_stripe from "../model/stripe.js";
|
||||||
|
import type * as model_users from "../model/users.js";
|
||||||
|
import type * as orders from "../orders.js";
|
||||||
|
import type * as products from "../products.js";
|
||||||
|
import type * as returnActions from "../returnActions.js";
|
||||||
|
import type * as reviews from "../reviews.js";
|
||||||
|
import type * as shippoWebhook from "../shippoWebhook.js";
|
||||||
|
import type * as stripeActions from "../stripeActions.js";
|
||||||
|
import type * as users from "../users.js";
|
||||||
|
import type * as wishlists from "../wishlists.js";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiFromModules,
|
||||||
|
FilterApi,
|
||||||
|
FunctionReference,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
declare const fullApi: ApiFromModules<{
|
||||||
|
addresses: typeof addresses;
|
||||||
|
adminInvitations: typeof adminInvitations;
|
||||||
|
carts: typeof carts;
|
||||||
|
categories: typeof categories;
|
||||||
|
checkout: typeof checkout;
|
||||||
|
checkoutActions: typeof checkoutActions;
|
||||||
|
emails: typeof emails;
|
||||||
|
fulfillmentActions: typeof fulfillmentActions;
|
||||||
|
http: typeof http;
|
||||||
|
"model/carts": typeof model_carts;
|
||||||
|
"model/categories": typeof model_categories;
|
||||||
|
"model/checkout": typeof model_checkout;
|
||||||
|
"model/orders": typeof model_orders;
|
||||||
|
"model/products": typeof model_products;
|
||||||
|
"model/shippo": typeof model_shippo;
|
||||||
|
"model/stripe": typeof model_stripe;
|
||||||
|
"model/users": typeof model_users;
|
||||||
|
orders: typeof orders;
|
||||||
|
products: typeof products;
|
||||||
|
returnActions: typeof returnActions;
|
||||||
|
reviews: typeof reviews;
|
||||||
|
shippoWebhook: typeof shippoWebhook;
|
||||||
|
stripeActions: typeof stripeActions;
|
||||||
|
users: typeof users;
|
||||||
|
wishlists: typeof wishlists;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's public API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export declare const api: FilterApi<
|
||||||
|
typeof fullApi,
|
||||||
|
FunctionReference<any, "public">
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's internal API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = internal.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export declare const internal: FilterApi<
|
||||||
|
typeof fullApi,
|
||||||
|
FunctionReference<any, "internal">
|
||||||
|
>;
|
||||||
|
|
||||||
|
export declare const components: {
|
||||||
|
resend: {
|
||||||
|
lib: {
|
||||||
|
cancelEmail: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ emailId: string },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
cleanupAbandonedEmails: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ olderThan?: number },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
cleanupOldEmails: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ olderThan?: number },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
createManualEmail: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
from: string;
|
||||||
|
headers?: Array<{ name: string; value: string }>;
|
||||||
|
replyTo?: Array<string>;
|
||||||
|
subject: string;
|
||||||
|
to: Array<string> | string;
|
||||||
|
},
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
get: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ emailId: string },
|
||||||
|
{
|
||||||
|
bcc?: Array<string>;
|
||||||
|
bounced?: boolean;
|
||||||
|
cc?: Array<string>;
|
||||||
|
clicked?: boolean;
|
||||||
|
complained: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
deliveryDelayed?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
failed?: boolean;
|
||||||
|
finalizedAt: number;
|
||||||
|
from: string;
|
||||||
|
headers?: Array<{ name: string; value: string }>;
|
||||||
|
html?: string;
|
||||||
|
opened: boolean;
|
||||||
|
replyTo: Array<string>;
|
||||||
|
resendId?: string;
|
||||||
|
segment: number;
|
||||||
|
status:
|
||||||
|
| "waiting"
|
||||||
|
| "queued"
|
||||||
|
| "cancelled"
|
||||||
|
| "sent"
|
||||||
|
| "delivered"
|
||||||
|
| "delivery_delayed"
|
||||||
|
| "bounced"
|
||||||
|
| "failed";
|
||||||
|
subject?: string;
|
||||||
|
template?: {
|
||||||
|
id: string;
|
||||||
|
variables?: Record<string, string | number>;
|
||||||
|
};
|
||||||
|
text?: string;
|
||||||
|
to: Array<string>;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getStatus: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ emailId: string },
|
||||||
|
{
|
||||||
|
bounced: boolean;
|
||||||
|
clicked: boolean;
|
||||||
|
complained: boolean;
|
||||||
|
deliveryDelayed: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
failed: boolean;
|
||||||
|
opened: boolean;
|
||||||
|
status:
|
||||||
|
| "waiting"
|
||||||
|
| "queued"
|
||||||
|
| "cancelled"
|
||||||
|
| "sent"
|
||||||
|
| "delivered"
|
||||||
|
| "delivery_delayed"
|
||||||
|
| "bounced"
|
||||||
|
| "failed";
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
handleEmailEvent: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ event: any },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
sendEmail: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
bcc?: Array<string>;
|
||||||
|
cc?: Array<string>;
|
||||||
|
from: string;
|
||||||
|
headers?: Array<{ name: string; value: string }>;
|
||||||
|
html?: string;
|
||||||
|
options: {
|
||||||
|
apiKey: string;
|
||||||
|
initialBackoffMs: number;
|
||||||
|
onEmailEvent?: { fnHandle: string };
|
||||||
|
retryAttempts: number;
|
||||||
|
testMode: boolean;
|
||||||
|
};
|
||||||
|
replyTo?: Array<string>;
|
||||||
|
subject?: string;
|
||||||
|
template?: {
|
||||||
|
id: string;
|
||||||
|
variables?: Record<string, string | number>;
|
||||||
|
};
|
||||||
|
text?: string;
|
||||||
|
to: Array<string>;
|
||||||
|
},
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
updateManualEmail: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
emailId: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
resendId?: string;
|
||||||
|
status:
|
||||||
|
| "waiting"
|
||||||
|
| "queued"
|
||||||
|
| "cancelled"
|
||||||
|
| "sent"
|
||||||
|
| "delivered"
|
||||||
|
| "delivery_delayed"
|
||||||
|
| "bounced"
|
||||||
|
| "failed";
|
||||||
|
},
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
23
convex/_generated/api.js
Normal file
23
convex/_generated/api.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated `api` utility.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { anyApi, componentsGeneric } from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```js
|
||||||
|
* const myFunctionReference = api.myModule.myFunction;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const api = anyApi;
|
||||||
|
export const internal = anyApi;
|
||||||
|
export const components = componentsGeneric();
|
||||||
60
convex/_generated/dataModel.d.ts
vendored
Normal file
60
convex/_generated/dataModel.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated data model types.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DataModelFromSchemaDefinition,
|
||||||
|
DocumentByName,
|
||||||
|
TableNamesInDataModel,
|
||||||
|
SystemTableNames,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { GenericId } from "convex/values";
|
||||||
|
import schema from "../schema.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The names of all of your Convex tables.
|
||||||
|
*/
|
||||||
|
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of a document stored in Convex.
|
||||||
|
*
|
||||||
|
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||||
|
*/
|
||||||
|
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||||
|
DataModel,
|
||||||
|
TableName
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An identifier for a document in Convex.
|
||||||
|
*
|
||||||
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
|
*
|
||||||
|
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||||
|
*
|
||||||
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
|
* strings when type checking.
|
||||||
|
*
|
||||||
|
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||||
|
*/
|
||||||
|
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||||
|
GenericId<TableName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type describing your Convex data model.
|
||||||
|
*
|
||||||
|
* This type includes information about what tables you have, the type of
|
||||||
|
* documents stored in those tables, and the indexes defined on them.
|
||||||
|
*
|
||||||
|
* This type is used to parameterize methods like `queryGeneric` and
|
||||||
|
* `mutationGeneric` to make them type-safe.
|
||||||
|
*/
|
||||||
|
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||||
143
convex/_generated/server.d.ts
vendored
Normal file
143
convex/_generated/server.d.ts
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionBuilder,
|
||||||
|
HttpActionBuilder,
|
||||||
|
MutationBuilder,
|
||||||
|
QueryBuilder,
|
||||||
|
GenericActionCtx,
|
||||||
|
GenericMutationCtx,
|
||||||
|
GenericQueryCtx,
|
||||||
|
GenericDatabaseReader,
|
||||||
|
GenericDatabaseWriter,
|
||||||
|
} from "convex/server";
|
||||||
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export declare const httpAction: HttpActionBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex query functions.
|
||||||
|
*
|
||||||
|
* The query context is passed as the first argument to any Convex query
|
||||||
|
* function run on the server.
|
||||||
|
*
|
||||||
|
* This differs from the {@link MutationCtx} because all of the services are
|
||||||
|
* read-only.
|
||||||
|
*/
|
||||||
|
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex mutation functions.
|
||||||
|
*
|
||||||
|
* The mutation context is passed as the first argument to any Convex mutation
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of services for use within Convex action functions.
|
||||||
|
*
|
||||||
|
* The action context is passed as the first argument to any Convex action
|
||||||
|
* function run on the server.
|
||||||
|
*/
|
||||||
|
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from the database within Convex query functions.
|
||||||
|
*
|
||||||
|
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||||
|
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||||
|
* building a query.
|
||||||
|
*/
|
||||||
|
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to read from and write to the database within Convex mutation
|
||||||
|
* functions.
|
||||||
|
*
|
||||||
|
* Convex guarantees that all writes within a single mutation are
|
||||||
|
* executed atomically, so you never have to worry about partial writes leaving
|
||||||
|
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||||
|
* for the guarantees Convex provides your functions.
|
||||||
|
*/
|
||||||
|
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||||
93
convex/_generated/server.js
Normal file
93
convex/_generated/server.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||||
|
*
|
||||||
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||||
|
*
|
||||||
|
* To regenerate, run `npx convex dev`.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
actionGeneric,
|
||||||
|
httpActionGeneric,
|
||||||
|
queryGeneric,
|
||||||
|
mutationGeneric,
|
||||||
|
internalActionGeneric,
|
||||||
|
internalMutationGeneric,
|
||||||
|
internalQueryGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const query = queryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||||
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalQuery = internalQueryGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const mutation = mutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||||
|
*
|
||||||
|
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||||
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalMutation = internalMutationGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action in this Convex app's public API.
|
||||||
|
*
|
||||||
|
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||||
|
* code and code with side-effects, like calling third-party services.
|
||||||
|
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||||
|
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||||
|
*
|
||||||
|
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const action = actionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||||
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
|
*/
|
||||||
|
export const internalAction = internalActionGeneric;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define an HTTP action.
|
||||||
|
*
|
||||||
|
* The wrapped function will be used to respond to HTTP requests received
|
||||||
|
* by a Convex deployment if the requests matches the path and method where
|
||||||
|
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||||
|
*
|
||||||
|
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||||
|
* and a Fetch API `Request` object as its second.
|
||||||
|
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||||
|
*/
|
||||||
|
export const httpAction = httpActionGeneric;
|
||||||
@@ -26,6 +26,8 @@ async function setupUserAndVariant(t: ReturnType<typeof convexTest>) {
|
|||||||
status: "active",
|
status: "active",
|
||||||
categoryId,
|
categoryId,
|
||||||
tags: [],
|
tags: [],
|
||||||
|
parentCategorySlug: "toys",
|
||||||
|
childCategorySlug: "toys",
|
||||||
});
|
});
|
||||||
variantId = await ctx.db.insert("productVariants", {
|
variantId = await ctx.db.insert("productVariants", {
|
||||||
productId,
|
productId,
|
||||||
@@ -35,6 +37,8 @@ async function setupUserAndVariant(t: ReturnType<typeof convexTest>) {
|
|||||||
stockQuantity: 50,
|
stockQuantity: 50,
|
||||||
attributes: { color: "Red" },
|
attributes: { color: "Red" },
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
weight: 100,
|
||||||
|
weightUnit: "g",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ async function setupFullCheckoutContext(
|
|||||||
productId,
|
productId,
|
||||||
name: "1kg Bag",
|
name: "1kg Bag",
|
||||||
sku: "PK-001",
|
sku: "PK-001",
|
||||||
price: overrides?.price ?? 24.99,
|
price: overrides?.price ?? 2499,
|
||||||
stockQuantity: overrides?.stockQuantity ?? 50,
|
stockQuantity: overrides?.stockQuantity ?? 50,
|
||||||
isActive: overrides?.isActive ?? true,
|
isActive: overrides?.isActive ?? true,
|
||||||
weight: 1000,
|
weight: 1000,
|
||||||
@@ -151,7 +151,7 @@ async function setupFullCheckoutContext(
|
|||||||
productId,
|
productId,
|
||||||
variantId,
|
variantId,
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
price: overrides?.price ?? 24.99,
|
price: overrides?.price ?? 2499,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -294,7 +294,7 @@ describe("stripeActions.createCheckoutSession", () => {
|
|||||||
price_data: {
|
price_data: {
|
||||||
currency: "gbp",
|
currency: "gbp",
|
||||||
product_data: { name: "Premium Kibble — 1kg Bag" },
|
product_data: { name: "Premium Kibble — 1kg Bag" },
|
||||||
unit_amount: Math.round(24.99 * 100),
|
unit_amount: 2499,
|
||||||
},
|
},
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
|
import path from "node:path";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "apps/storefront/src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: "edge-runtime",
|
environment: "edge-runtime",
|
||||||
server: { deps: { inline: ["convex-test"] } },
|
server: { deps: { inline: ["convex-test"] } },
|
||||||
|
onUnhandledError(error): boolean | void {
|
||||||
|
const msg = error?.message ?? String(error);
|
||||||
|
if (
|
||||||
|
typeof msg === "string" &&
|
||||||
|
msg.includes("Write outside of transaction") &&
|
||||||
|
msg.includes("_scheduled_functions")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user