admoin
This commit is contained in:
parent
841a6bdc74
commit
cb29331d58
7 changed files with 149 additions and 187 deletions
|
|
@ -39,14 +39,12 @@ describe('TestPacksManager', () => {
|
||||||
title: 'Pack One',
|
title: 'Pack One',
|
||||||
cards: 10,
|
cards: 10,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pack-2',
|
id: 'pack-2',
|
||||||
title: 'Pack Two',
|
title: 'Pack Two',
|
||||||
cards: 5,
|
cards: 5,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
order: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
total: 2,
|
total: 2,
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ function TestPacksManagerInner({
|
||||||
const fallbackId = Array.from(nextSelected)[0]
|
const fallbackId = Array.from(nextSelected)[0]
|
||||||
const fallback = fallbackId ? packsById.get(fallbackId) : undefined
|
const fallback = fallbackId ? packsById.get(fallbackId) : undefined
|
||||||
if (fallback?.color) onSelectedPackColorChange(fallback.color)
|
if (fallback?.color) onSelectedPackColorChange(fallback.color)
|
||||||
|
else onSelectedPackColorChange('')
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -140,8 +141,8 @@ function TestPacksManagerInner({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onSelectedPackColorChange) {
|
if (onSelectedPackColorChange && pack.color) {
|
||||||
onSelectedPackColorChange(pack.color ?? '')
|
onSelectedPackColorChange(pack.color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export function InputButtonsQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Question Image"
|
label="Question Image"
|
||||||
value={image}
|
value={image}
|
||||||
onChange={(value) => setImage(value || '')}
|
onChange={(value: string | undefined) => setImage(value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -181,7 +181,7 @@ export function InputButtonsQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Image"
|
label="Image"
|
||||||
value={button.image || ''}
|
value={button.image || ''}
|
||||||
onChange={(value) =>
|
onChange={(value: string | undefined) =>
|
||||||
updateButton(index, { image: value || undefined })
|
updateButton(index, { image: value || undefined })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export function SimpleQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Question Image"
|
label="Question Image"
|
||||||
value={image}
|
value={image}
|
||||||
onChange={(value) => setImage(value || '')}
|
onChange={(value: string | undefined) => setImage(value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ export function SimpleQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Image"
|
label="Image"
|
||||||
value={button.image || ''}
|
value={button.image || ''}
|
||||||
onChange={(value) =>
|
onChange={(value: string | undefined) =>
|
||||||
updateButton(index, { image: value || undefined })
|
updateButton(index, { image: value || undefined })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
77
admin/src/components/ui/image-upload.tsx
Normal file
77
admin/src/components/ui/image-upload.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ImageIcon, X } from 'lucide-react'
|
||||||
|
|
||||||
|
interface ImageUploadProps {
|
||||||
|
label?: string
|
||||||
|
value?: string
|
||||||
|
onChange: (value: string | undefined) => void
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageUpload({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
placeholder = 'Enter image URL',
|
||||||
|
}: ImageUploadProps) {
|
||||||
|
const [url, setUrl] = useState(value || '')
|
||||||
|
|
||||||
|
const handleChange = (newValue: string) => {
|
||||||
|
setUrl(newValue)
|
||||||
|
onChange(newValue.trim() || undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setUrl('')
|
||||||
|
onChange(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{label && <Label>{label}</Label>}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<ImageIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{url && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClear}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{url && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-32 w-auto rounded border"
|
||||||
|
onError={(e) => {
|
||||||
|
// Hide broken images
|
||||||
|
e.currentTarget.style.display = 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { packsApi, isPacksApiError } from '@/api/packs'
|
import { packsApi, isPacksApiError } from '@/api/packs'
|
||||||
import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils'
|
import { formatApiError, getDetailedErrorMessage } from '@/lib/error-utils'
|
||||||
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse } from '@/types/models'
|
import type { EditCardPackDto, CardPackPreviewDto, PaginatedResponse, Question } from '@/types/models'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
|
@ -36,8 +36,6 @@ import {
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { ImageUpload } from '@/components/ui/image-upload'
|
|
||||||
import { ColorPaletteInput } from '@/components/ui/color-palette-input'
|
|
||||||
import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'
|
import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
|
|
||||||
export default function PacksPage() {
|
export default function PacksPage() {
|
||||||
|
|
@ -52,19 +50,11 @@ export default function PacksPage() {
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
name: '',
|
||||||
subtitle: '',
|
|
||||||
description: '',
|
description: '',
|
||||||
color: '',
|
category: '',
|
||||||
enabled: true,
|
isPublic: true,
|
||||||
googlePlayId: '',
|
questions: [] as Question[],
|
||||||
rustoreId: '',
|
|
||||||
appStoreId: '',
|
|
||||||
price: '',
|
|
||||||
order: 0,
|
|
||||||
version: '',
|
|
||||||
size: 0,
|
|
||||||
cover: undefined as string | undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -136,19 +126,11 @@ export default function PacksPage() {
|
||||||
const openCreateDialog = () => {
|
const openCreateDialog = () => {
|
||||||
setSelectedPack(null)
|
setSelectedPack(null)
|
||||||
setFormData({
|
setFormData({
|
||||||
title: '',
|
name: '',
|
||||||
subtitle: '',
|
|
||||||
description: '',
|
description: '',
|
||||||
color: '',
|
category: '',
|
||||||
enabled: true,
|
isPublic: true,
|
||||||
googlePlayId: '',
|
questions: [],
|
||||||
rustoreId: '',
|
|
||||||
appStoreId: '',
|
|
||||||
price: '',
|
|
||||||
order: 0,
|
|
||||||
version: '',
|
|
||||||
size: 0,
|
|
||||||
cover: undefined,
|
|
||||||
})
|
})
|
||||||
setIsDialogOpen(true)
|
setIsDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
@ -158,19 +140,11 @@ export default function PacksPage() {
|
||||||
const fullPack = await packsApi.getPack(pack.id)
|
const fullPack = await packsApi.getPack(pack.id)
|
||||||
setSelectedPack(fullPack)
|
setSelectedPack(fullPack)
|
||||||
setFormData({
|
setFormData({
|
||||||
title: fullPack.title || '',
|
name: fullPack.name || '',
|
||||||
subtitle: fullPack.subtitle || '',
|
|
||||||
description: fullPack.description || '',
|
description: fullPack.description || '',
|
||||||
color: fullPack.color || '',
|
category: fullPack.category || '',
|
||||||
enabled: fullPack.enabled ?? true,
|
isPublic: fullPack.isPublic ?? true,
|
||||||
googlePlayId: fullPack.googlePlayId || '',
|
questions: fullPack.questions || [],
|
||||||
rustoreId: fullPack.rustoreId || '',
|
|
||||||
appStoreId: fullPack.appStoreId || '',
|
|
||||||
price: fullPack.price || '',
|
|
||||||
order: fullPack.order || 0,
|
|
||||||
version: fullPack.version || '',
|
|
||||||
size: fullPack.size || 0,
|
|
||||||
cover: fullPack.cover,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsDialogOpen(true)
|
setIsDialogOpen(true)
|
||||||
|
|
@ -191,27 +165,29 @@ export default function PacksPage() {
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (!formData.title.trim()) {
|
if (!formData.name.trim()) {
|
||||||
toast.error('Title is required')
|
toast.error('Name is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.description.trim()) {
|
||||||
|
toast.error('Description is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.category.trim()) {
|
||||||
|
toast.error('Category is required')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const packData: EditCardPackDto = {
|
const packData: EditCardPackDto = {
|
||||||
// Only include id for updates, not for new packs
|
// Only include id for updates, not for new packs
|
||||||
...(selectedPack && { id: selectedPack.id }),
|
...(selectedPack && { id: selectedPack.id }),
|
||||||
title: formData.title.trim(),
|
name: formData.name.trim(),
|
||||||
subtitle: formData.subtitle.trim() || undefined,
|
description: formData.description.trim(),
|
||||||
description: formData.description.trim() || undefined,
|
category: formData.category.trim(),
|
||||||
color: formData.color.trim() || undefined,
|
isPublic: formData.isPublic,
|
||||||
enabled: formData.enabled,
|
questions: formData.questions,
|
||||||
googlePlayId: formData.googlePlayId.trim() || undefined,
|
|
||||||
rustoreId: formData.rustoreId.trim() || undefined,
|
|
||||||
appStoreId: formData.appStoreId.trim() || undefined,
|
|
||||||
price: formData.price.trim() || undefined,
|
|
||||||
order: formData.order,
|
|
||||||
version: formData.version.trim() || undefined,
|
|
||||||
size: formData.size > 0 ? formData.size : undefined,
|
|
||||||
cover: formData.cover || undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPack) {
|
if (selectedPack) {
|
||||||
|
|
@ -336,12 +312,7 @@ export default function PacksPage() {
|
||||||
<TableRow key={pack.id}>
|
<TableRow key={pack.id}>
|
||||||
<TableCell className="font-mono text-sm">{pack.id}</TableCell>
|
<TableCell className="font-mono text-sm">{pack.id}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div>
|
<div className="font-medium">{pack.title}</div>
|
||||||
<div className="font-medium">{pack.title}</div>
|
|
||||||
{pack.subtitle && (
|
|
||||||
<div className="text-sm text-muted-foreground">{pack.subtitle}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{pack.cards}</TableCell>
|
<TableCell>{pack.cards}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
@ -426,146 +397,57 @@ export default function PacksPage() {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="name">Name *</Label>
|
||||||
<Label htmlFor="title">Title *</Label>
|
<Input
|
||||||
<Input
|
id="name"
|
||||||
id="title"
|
value={formData.name}
|
||||||
value={formData.title}
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
placeholder="Pack name"
|
||||||
placeholder="Pack title"
|
required
|
||||||
required
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="subtitle">Subtitle</Label>
|
|
||||||
<Input
|
|
||||||
id="subtitle"
|
|
||||||
value={formData.subtitle}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, subtitle: e.target.value }))}
|
|
||||||
placeholder="Pack subtitle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description *</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
placeholder="Pack description"
|
placeholder="Pack description"
|
||||||
rows={3}
|
rows={3}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="category">Category *</Label>
|
||||||
<Label htmlFor="color">Color</Label>
|
<Input
|
||||||
<ColorPaletteInput
|
id="category"
|
||||||
id="color"
|
value={formData.category}
|
||||||
value={formData.color}
|
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
|
||||||
onChange={(value) => setFormData(prev => ({ ...prev, color: value }))}
|
placeholder="Pack category"
|
||||||
disabled={createMutation.isPending || updateMutation.isPending}
|
required
|
||||||
placeholder="#FF0000"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="order">Order</Label>
|
|
||||||
<Input
|
|
||||||
id="order"
|
|
||||||
type="number"
|
|
||||||
value={formData.order}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 0 }))}
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="enabled"
|
id="isPublic"
|
||||||
checked={formData.enabled}
|
checked={formData.isPublic}
|
||||||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, enabled: checked as boolean }))}
|
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isPublic: checked as boolean }))}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="enabled">Enabled</Label>
|
<Label htmlFor="isPublic">Public</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Store IDs</Label>
|
<Label>Questions</Label>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="space-y-2">
|
{formData.questions.length} question(s) in this pack
|
||||||
<Label htmlFor="googlePlayId" className="text-xs">Google Play</Label>
|
</p>
|
||||||
<Input
|
<p className="text-xs text-muted-foreground">
|
||||||
id="googlePlayId"
|
Questions can be managed through the question editor
|
||||||
value={formData.googlePlayId}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, googlePlayId: e.target.value }))}
|
|
||||||
placeholder="com.example.app"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="rustoreId" className="text-xs">RuStore</Label>
|
|
||||||
<Input
|
|
||||||
id="rustoreId"
|
|
||||||
value={formData.rustoreId}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, rustoreId: e.target.value }))}
|
|
||||||
placeholder="123456789"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="appStoreId" className="text-xs">App Store</Label>
|
|
||||||
<Input
|
|
||||||
id="appStoreId"
|
|
||||||
value={formData.appStoreId}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, appStoreId: e.target.value }))}
|
|
||||||
placeholder="1234567890"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="price">Price</Label>
|
|
||||||
<Input
|
|
||||||
id="price"
|
|
||||||
value={formData.price}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, price: e.target.value }))}
|
|
||||||
placeholder="Free, $1.99, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="version">Version</Label>
|
|
||||||
<Input
|
|
||||||
id="version"
|
|
||||||
value={formData.version}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, version: e.target.value }))}
|
|
||||||
placeholder="1.0.0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="size">Size</Label>
|
|
||||||
<Input
|
|
||||||
id="size"
|
|
||||||
type="number"
|
|
||||||
value={formData.size}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, size: parseInt(e.target.value) || 0 }))}
|
|
||||||
placeholder="0"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Number of cards in the pack (auto-calculated if not set)
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<ImageUpload
|
|
||||||
label="Cover Image"
|
|
||||||
value={formData.cover}
|
|
||||||
onChange={(value) => setFormData(prev => ({ ...prev, cover: value }))}
|
|
||||||
disabled={createMutation.isPending || updateMutation.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,10 @@ export interface CardPackPreviewDto {
|
||||||
title: string
|
title: string
|
||||||
cards: number
|
cards: number
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
// Optional properties that may be used in UI but not in backend
|
||||||
|
subtitle?: string
|
||||||
|
color?: string
|
||||||
|
order?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// User model matching backend Prisma schema
|
// User model matching backend Prisma schema
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue