admin: Superforms + Zod validation, three-address-defaults schema, profiles toast fix

- Add Superforms + Zod 4 for all admin form validation
- Replace address isDefault with three separate boolean defaults (Mailing, Billing, Shipping)
- Update profiles page with three-checkbox address form and toast improvements
- Convert all admin page server actions to pass request directly to crud
- Export superFormErrorsToFieldErrors helper for profiles server actions
- Regenerate Prisma client for new schema fields
This commit is contained in:
NeilClack
2026-07-07 13:36:23 -05:00
parent 38347f5f24
commit 38d1752f5b
98 changed files with 3408 additions and 1758 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Session, User } from "better-auth";
import type { Session, User } from 'better-auth';
declare global {
namespace App {
@@ -9,4 +9,4 @@ declare global {
}
}
export { };
export {};
+22 -21
View File
@@ -1,24 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
const theme = localStorage.getItem('theme');
if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
%sveltekit.head%
</head>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
%sveltekit.head%
</head>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="h-dvh w-dvw flex flex-col">%sveltekit.body%</div>
</body>
</html>
<body class="m-0" data-sveltekit-preload-data="hover">
<div class="flex h-dvh w-dvw flex-col">%sveltekit.body%</div>
</body>
</html>
+1 -3
View File
@@ -87,9 +87,7 @@ describe('hooks.server handle', () => {
const event = createMockEvent();
await handle({ event, resolve: event.resolve });
expect(svelteKitHandler).toHaveBeenCalledWith(
expect.objectContaining({ building: false })
);
expect(svelteKitHandler).toHaveBeenCalledWith(expect.objectContaining({ building: false }));
});
it('calls svelteKitHandler with auth instance', async () => {
+14 -14
View File
@@ -1,18 +1,18 @@
import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from "$app/environment";
import { auth } from '$lib/auth';
import { svelteKitHandler } from 'better-auth/svelte-kit';
import { building } from '$app/environment';
export async function handle({ event, resolve }) {
// Fetch current session from Better Auth
const session = await auth.api.getSession({
headers: event.request.headers,
});
// Fetch current session from Better Auth
const session = await auth.api.getSession({
headers: event.request.headers
});
// Make session and user available on server
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
}
// Make session and user available on server
if (session) {
event.locals.session = session.session;
event.locals.user = session.user;
}
return svelteKitHandler({ event, resolve, auth, building });
}
return svelteKitHandler({ event, resolve, auth, building });
}
+2 -2
View File
@@ -1,3 +1,3 @@
import { createAuthClient } from "better-auth/svelte"
import { createAuthClient } from 'better-auth/svelte';
export const authClient = createAuthClient({});
export const authClient = createAuthClient({});
+1 -4
View File
@@ -46,9 +46,6 @@ export const auth = betterAuth({
window: 60,
max: 100
},
trustedOrigins: [
'http://localhost:5173',
'https://portal.rhemawavetech.com'
],
trustedOrigins: ['http://localhost:5173', 'https://portal.rhemawavetech.com'],
plugins: [sveltekitCookies(getRequestEvent)]
});
+92
View File
@@ -0,0 +1,92 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Highlight from '@tiptap/extension-highlight';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-color';
let { initialContent = '', name = 'content' }: { initialContent?: string; name?: string } =
$props();
let element: HTMLElement;
let editor: Editor;
let content = $state('');
onMount(() => {
content = initialContent;
editor = new Editor({
element,
extensions: [
StarterKit.configure({
bulletList: false,
orderedList: false
}),
TextStyle,
Color,
Highlight.configure({ multicolor: true })
],
content: initialContent,
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none outline-none h-full min-h-40 p-3 dark:prose-invert dark:text-gray-100'
}
},
onUpdate: ({ editor }) => {
content = editor.getHTML();
}
});
editor.setEditable(true);
return () => editor.destroy();
});
function toggleBold() { editor?.chain().focus().toggleBold().run(); }
function toggleItalic() { editor?.chain().focus().toggleItalic().run(); }
function toggleHighlight() { editor?.chain().focus().toggleHighlight().run(); }
function setColor() {
const current = editor?.getAttributes('textStyle').color ?? '#000000';
const colors = ['#000000', '#FF0000', '#0000FF', '#008000', '#FFA500', '#800080', '#808080'];
const idx = colors.indexOf(current);
const next = colors[(idx + 1) % colors.length];
editor?.chain().focus().setColor(next).run();
}
</script>
<div class="border border-gray-300 dark:border-gray-600">
<div
class="flex gap-1 border-b border-gray-300 bg-gray-100 p-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300"
>
<button
type="button"
onclick={toggleBold}
class="min-w-8 px-2 py-1 font-bold hover:bg-gray-200 dark:hover:bg-gray-700"
title="Bold">B</button>
<button
type="button"
onclick={toggleItalic}
class="min-w-8 px-2 py-1 italic hover:bg-gray-200 dark:hover:bg-gray-700"
title="Italic">I</button>
<button
type="button"
onclick={toggleHighlight}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Highlight"><span class="bg-yellow-200 px-1 dark:bg-yellow-600">H</span></button>
<button
type="button"
onclick={setColor}
class="min-w-8 px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-700"
title="Cycle text color">A</button>
</div>
<div
bind:this={element}
role="textbox"
tabindex="0"
contenteditable="true"
class="min-h-40 cursor-text bg-white dark:bg-gray-900"
></div>
</div>
<input type="hidden" {name} value={content} />
+3 -1
View File
@@ -28,7 +28,9 @@ export * from "./enums.ts"
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
@@ -202,33 +202,6 @@ export type IntWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -430,20 +403,4 @@ export type NestedFloatFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
File diff suppressed because one or more lines are too long
@@ -80,12 +80,12 @@ export type PrismaVersion = {
}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/
export const prismaVersion: PrismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
}
/**
@@ -1436,7 +1436,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber'
phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -1449,7 +1450,11 @@ export const AddressScalarFieldEnum = {
city: 'city',
state: 'state',
postalCode: 'postalCode',
country: 'country'
country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -1514,7 +1519,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId'
} as const
@@ -1754,6 +1758,21 @@ export type PrismaClientOptions = ({
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
/**
* Optional maximum size for the query plan cache. If not provided, a default size will be used.
* A value of `0` can be used to disable the cache entirely. A higher cache size can improve
* performance for applications that execute a large number of unique queries, while a smaller
* cache size can reduce memory usage.
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* queryPlanCacheMaxSize: 100,
* })
* ```
*/
queryPlanCacheMaxSize?: number
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
@@ -72,12 +72,12 @@ export type ModelName = (typeof ModelName)[keyof typeof ModelName]
* Enums
*/
export const TransactionIsolationLevel = {
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
@@ -101,7 +101,8 @@ export const ProfileScalarFieldEnum = {
userId: 'userId',
firstName: 'firstName',
lastName: 'lastName',
phoneNumber: 'phoneNumber'
phoneNumber: 'phoneNumber',
ghlContactId: 'ghlContactId'
} as const
export type ProfileScalarFieldEnum = (typeof ProfileScalarFieldEnum)[keyof typeof ProfileScalarFieldEnum]
@@ -114,7 +115,11 @@ export const AddressScalarFieldEnum = {
city: 'city',
state: 'state',
postalCode: 'postalCode',
country: 'country'
country: 'country',
type: 'type',
isDefaultMailing: 'isDefaultMailing',
isDefaultBilling: 'isDefaultBilling',
isDefaultShipping: 'isDefaultShipping'
} as const
export type AddressScalarFieldEnum = (typeof AddressScalarFieldEnum)[keyof typeof AddressScalarFieldEnum]
@@ -179,7 +184,6 @@ export const ProclamationScalarFieldEnum = {
content: 'content',
solfeggioFrequencyId: 'solfeggioFrequencyId',
categoryId: 'categoryId',
price: 'price',
subscriptionLevelId: 'subscriptionLevelId'
} as const
+6 -1
View File
@@ -214,7 +214,7 @@ export type AccountGroupByOutputType = {
_max: AccountMaxAggregateOutputType | null
}
type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise<
export type GetAccountGroupByPayload<T extends AccountGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<AccountGroupByOutputType, T['by']> &
{
@@ -1406,6 +1406,11 @@ export type AccountFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Accounts.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Accounts.
*/
distinct?: Prisma.AccountScalarFieldEnum | Prisma.AccountScalarFieldEnum[]
}
+147 -2
View File
@@ -32,6 +32,10 @@ export type AddressMinAggregateOutputType = {
state: string | null
postalCode: string | null
country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
}
export type AddressMaxAggregateOutputType = {
@@ -42,6 +46,10 @@ export type AddressMaxAggregateOutputType = {
state: string | null
postalCode: string | null
country: string | null
type: string | null
isDefaultMailing: boolean | null
isDefaultBilling: boolean | null
isDefaultShipping: boolean | null
}
export type AddressCountAggregateOutputType = {
@@ -52,6 +60,10 @@ export type AddressCountAggregateOutputType = {
state: number
postalCode: number
country: number
type: number
isDefaultMailing: number
isDefaultBilling: number
isDefaultShipping: number
_all: number
}
@@ -64,6 +76,10 @@ export type AddressMinAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
}
export type AddressMaxAggregateInputType = {
@@ -74,6 +90,10 @@ export type AddressMaxAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
}
export type AddressCountAggregateInputType = {
@@ -84,6 +104,10 @@ export type AddressCountAggregateInputType = {
state?: true
postalCode?: true
country?: true
type?: true
isDefaultMailing?: true
isDefaultBilling?: true
isDefaultShipping?: true
_all?: true
}
@@ -167,12 +191,16 @@ export type AddressGroupByOutputType = {
state: string
postalCode: string
country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
_count: AddressCountAggregateOutputType | null
_min: AddressMinAggregateOutputType | null
_max: AddressMaxAggregateOutputType | null
}
type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise<
export type GetAddressGroupByPayload<T extends AddressGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<AddressGroupByOutputType, T['by']> &
{
@@ -198,6 +226,10 @@ export type AddressWhereInput = {
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
}
@@ -209,6 +241,10 @@ export type AddressOrderByWithRelationInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
profile?: Prisma.ProfileOrderByWithRelationInput
}
@@ -223,6 +259,10 @@ export type AddressWhereUniqueInput = Prisma.AtLeast<{
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
profile?: Prisma.XOR<Prisma.ProfileScalarRelationFilter, Prisma.ProfileWhereInput>
}, "id">
@@ -234,6 +274,10 @@ export type AddressOrderByWithAggregationInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
_count?: Prisma.AddressCountOrderByAggregateInput
_max?: Prisma.AddressMaxOrderByAggregateInput
_min?: Prisma.AddressMinOrderByAggregateInput
@@ -250,6 +294,10 @@ export type AddressScalarWhereWithAggregatesInput = {
state?: Prisma.StringWithAggregatesFilter<"Address"> | string
postalCode?: Prisma.StringWithAggregatesFilter<"Address"> | string
country?: Prisma.StringWithAggregatesFilter<"Address"> | string
type?: Prisma.StringWithAggregatesFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolWithAggregatesFilter<"Address"> | boolean
}
export type AddressCreateInput = {
@@ -259,6 +307,10 @@ export type AddressCreateInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile: Prisma.ProfileCreateNestedOneWithoutAddressesInput
}
@@ -270,6 +322,10 @@ export type AddressUncheckedCreateInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateInput = {
@@ -279,6 +335,10 @@ export type AddressUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
profile?: Prisma.ProfileUpdateOneRequiredWithoutAddressesNestedInput
}
@@ -290,6 +350,10 @@ export type AddressUncheckedUpdateInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressCreateManyInput = {
@@ -300,6 +364,10 @@ export type AddressCreateManyInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateManyMutationInput = {
@@ -309,6 +377,10 @@ export type AddressUpdateManyMutationInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateManyInput = {
@@ -319,6 +391,10 @@ export type AddressUncheckedUpdateManyInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressListRelationFilter = {
@@ -339,6 +415,10 @@ export type AddressCountOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressMaxOrderByAggregateInput = {
@@ -349,6 +429,10 @@ export type AddressMaxOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressMinOrderByAggregateInput = {
@@ -359,6 +443,10 @@ export type AddressMinOrderByAggregateInput = {
state?: Prisma.SortOrder
postalCode?: Prisma.SortOrder
country?: Prisma.SortOrder
type?: Prisma.SortOrder
isDefaultMailing?: Prisma.SortOrder
isDefaultBilling?: Prisma.SortOrder
isDefaultShipping?: Prisma.SortOrder
}
export type AddressCreateNestedManyWithoutProfileInput = {
@@ -410,6 +498,10 @@ export type AddressCreateWithoutProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUncheckedCreateWithoutProfileInput = {
@@ -419,6 +511,10 @@ export type AddressUncheckedCreateWithoutProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressCreateOrConnectWithoutProfileInput = {
@@ -458,6 +554,10 @@ export type AddressScalarWhereInput = {
state?: Prisma.StringFilter<"Address"> | string
postalCode?: Prisma.StringFilter<"Address"> | string
country?: Prisma.StringFilter<"Address"> | string
type?: Prisma.StringFilter<"Address"> | string
isDefaultMailing?: Prisma.BoolFilter<"Address"> | boolean
isDefaultBilling?: Prisma.BoolFilter<"Address"> | boolean
isDefaultShipping?: Prisma.BoolFilter<"Address"> | boolean
}
export type AddressCreateManyProfileInput = {
@@ -467,6 +567,10 @@ export type AddressCreateManyProfileInput = {
state: string
postalCode: string
country: string
type?: string
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressUpdateWithoutProfileInput = {
@@ -476,6 +580,10 @@ export type AddressUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateWithoutProfileInput = {
@@ -485,6 +593,10 @@ export type AddressUncheckedUpdateWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type AddressUncheckedUpdateManyWithoutProfileInput = {
@@ -494,6 +606,10 @@ export type AddressUncheckedUpdateManyWithoutProfileInput = {
state?: Prisma.StringFieldUpdateOperationsInput | string
postalCode?: Prisma.StringFieldUpdateOperationsInput | string
country?: Prisma.StringFieldUpdateOperationsInput | string
type?: Prisma.StringFieldUpdateOperationsInput | string
isDefaultMailing?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultBilling?: Prisma.BoolFieldUpdateOperationsInput | boolean
isDefaultShipping?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
@@ -506,6 +622,10 @@ export type AddressSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -517,6 +637,10 @@ export type AddressSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -528,6 +652,10 @@ export type AddressSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}, ExtArgs["result"]["address"]>
@@ -539,9 +667,13 @@ export type AddressSelectScalar = {
state?: boolean
postalCode?: boolean
country?: boolean
type?: boolean
isDefaultMailing?: boolean
isDefaultBilling?: boolean
isDefaultShipping?: boolean
}
export type AddressOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "profileId" | "street" | "city" | "state" | "postalCode" | "country", ExtArgs["result"]["address"]>
export type AddressOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "profileId" | "street" | "city" | "state" | "postalCode" | "country" | "type" | "isDefaultMailing" | "isDefaultBilling" | "isDefaultShipping", ExtArgs["result"]["address"]>
export type AddressInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
profile?: boolean | Prisma.ProfileDefaultArgs<ExtArgs>
}
@@ -565,6 +697,10 @@ export type $AddressPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
state: string
postalCode: string
country: string
type: string
isDefaultMailing: boolean
isDefaultBilling: boolean
isDefaultShipping: boolean
}, ExtArgs["result"]["address"]>
composites: {}
}
@@ -996,6 +1132,10 @@ export interface AddressFieldRefs {
readonly state: Prisma.FieldRef<"Address", 'String'>
readonly postalCode: Prisma.FieldRef<"Address", 'String'>
readonly country: Prisma.FieldRef<"Address", 'String'>
readonly type: Prisma.FieldRef<"Address", 'String'>
readonly isDefaultMailing: Prisma.FieldRef<"Address", 'Boolean'>
readonly isDefaultBilling: Prisma.FieldRef<"Address", 'Boolean'>
readonly isDefaultShipping: Prisma.FieldRef<"Address", 'Boolean'>
}
@@ -1192,6 +1332,11 @@ export type AddressFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Addresses.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Addresses.
*/
distinct?: Prisma.AddressScalarFieldEnum | Prisma.AddressScalarFieldEnum[]
}
@@ -158,7 +158,7 @@ export type LicenseKeyGroupByOutputType = {
_max: LicenseKeyMaxAggregateOutputType | null
}
type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise<
export type GetLicenseKeyGroupByPayload<T extends LicenseKeyGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<LicenseKeyGroupByOutputType, T['by']> &
{
@@ -1122,6 +1122,11 @@ export type LicenseKeyFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inte
* Skip the first `n` LicenseKeys.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of LicenseKeys.
*/
distinct?: Prisma.LicenseKeyScalarFieldEnum | Prisma.LicenseKeyScalarFieldEnum[]
}
@@ -29,14 +29,12 @@ export type AggregateProclamation = {
export type ProclamationAvgAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
export type ProclamationSumAggregateOutputType = {
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -46,7 +44,6 @@ export type ProclamationMinAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -56,7 +53,6 @@ export type ProclamationMaxAggregateOutputType = {
content: string | null
solfeggioFrequencyId: number | null
categoryId: number | null
price: number | null
subscriptionLevelId: number | null
}
@@ -66,7 +62,6 @@ export type ProclamationCountAggregateOutputType = {
content: number
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
_all: number
}
@@ -75,14 +70,12 @@ export type ProclamationCountAggregateOutputType = {
export type ProclamationAvgAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
export type ProclamationSumAggregateInputType = {
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -92,7 +85,6 @@ export type ProclamationMinAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -102,7 +94,6 @@ export type ProclamationMaxAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
}
@@ -112,7 +103,6 @@ export type ProclamationCountAggregateInputType = {
content?: true
solfeggioFrequencyId?: true
categoryId?: true
price?: true
subscriptionLevelId?: true
_all?: true
}
@@ -209,7 +199,6 @@ export type ProclamationGroupByOutputType = {
content: string
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
_count: ProclamationCountAggregateOutputType | null
_avg: ProclamationAvgAggregateOutputType | null
@@ -218,7 +207,7 @@ export type ProclamationGroupByOutputType = {
_max: ProclamationMaxAggregateOutputType | null
}
type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise<
export type GetProclamationGroupByPayload<T extends ProclamationGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProclamationGroupByOutputType, T['by']> &
{
@@ -242,7 +231,6 @@ export type ProclamationWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -256,7 +244,6 @@ export type ProclamationOrderByWithRelationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
solfeggioFrequency?: Prisma.SolfeggioFrequencyOrderByWithRelationInput
category?: Prisma.ProclamationCategoryOrderByWithRelationInput
@@ -273,7 +260,6 @@ export type ProclamationWhereUniqueInput = Prisma.AtLeast<{
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
solfeggioFrequency?: Prisma.XOR<Prisma.SolfeggioFrequencyScalarRelationFilter, Prisma.SolfeggioFrequencyWhereInput>
category?: Prisma.XOR<Prisma.ProclamationCategoryScalarRelationFilter, Prisma.ProclamationCategoryWhereInput>
@@ -287,7 +273,6 @@ export type ProclamationOrderByWithAggregationInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
_count?: Prisma.ProclamationCountOrderByAggregateInput
_avg?: Prisma.ProclamationAvgOrderByAggregateInput
@@ -305,7 +290,6 @@ export type ProclamationScalarWhereWithAggregatesInput = {
content?: Prisma.StringWithAggregatesFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
categoryId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
price?: Prisma.FloatWithAggregatesFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntWithAggregatesFilter<"Proclamation"> | number
}
@@ -313,7 +297,6 @@ export type ProclamationCreateInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -326,7 +309,6 @@ export type ProclamationUncheckedCreateInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -335,7 +317,6 @@ export type ProclamationUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -348,7 +329,6 @@ export type ProclamationUncheckedUpdateInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -359,7 +339,6 @@ export type ProclamationCreateManyInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -367,7 +346,6 @@ export type ProclamationUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
}
export type ProclamationUncheckedUpdateManyInput = {
@@ -376,7 +354,6 @@ export type ProclamationUncheckedUpdateManyInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -386,14 +363,12 @@ export type ProclamationCountOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
export type ProclamationAvgOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -403,7 +378,6 @@ export type ProclamationMaxOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -413,14 +387,12 @@ export type ProclamationMinOrderByAggregateInput = {
content?: Prisma.SortOrder
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
export type ProclamationSumOrderByAggregateInput = {
solfeggioFrequencyId?: Prisma.SortOrder
categoryId?: Prisma.SortOrder
price?: Prisma.SortOrder
subscriptionLevelId?: Prisma.SortOrder
}
@@ -439,14 +411,6 @@ export type ProclamationOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type FloatFieldUpdateOperationsInput = {
set?: number
increment?: number
decrement?: number
multiply?: number
divide?: number
}
export type ProclamationCreateNestedOneWithoutUserProclamationsInput = {
create?: Prisma.XOR<Prisma.ProclamationCreateWithoutUserProclamationsInput, Prisma.ProclamationUncheckedCreateWithoutUserProclamationsInput>
connectOrCreate?: Prisma.ProclamationCreateOrConnectWithoutUserProclamationsInput
@@ -591,7 +555,6 @@ export type ProclamationCreateWithoutUserProclamationsInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -603,7 +566,6 @@ export type ProclamationUncheckedCreateWithoutUserProclamationsInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -627,7 +589,6 @@ export type ProclamationUpdateWithoutUserProclamationsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -639,7 +600,6 @@ export type ProclamationUncheckedUpdateWithoutUserProclamationsInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -647,7 +607,6 @@ export type ProclamationCreateWithoutSolfeggioFrequencyInput = {
id?: string
name: string
content: string
price?: number
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -658,7 +617,6 @@ export type ProclamationUncheckedCreateWithoutSolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -698,7 +656,6 @@ export type ProclamationScalarWhereInput = {
content?: Prisma.StringFilter<"Proclamation"> | string
solfeggioFrequencyId?: Prisma.IntFilter<"Proclamation"> | number
categoryId?: Prisma.IntFilter<"Proclamation"> | number
price?: Prisma.FloatFilter<"Proclamation"> | number
subscriptionLevelId?: Prisma.IntFilter<"Proclamation"> | number
}
@@ -706,7 +663,6 @@ export type ProclamationCreateWithoutCategoryInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
subscriptionLevel?: Prisma.SubscriptionLevelCreateNestedOneWithoutProclamationsInput
@@ -717,7 +673,6 @@ export type ProclamationUncheckedCreateWithoutCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -752,7 +707,6 @@ export type ProclamationCreateWithoutSubscriptionLevelInput = {
id?: string
name: string
content: string
price?: number
solfeggioFrequency: Prisma.SolfeggioFrequencyCreateNestedOneWithoutProclamationsInput
category: Prisma.ProclamationCategoryCreateNestedOneWithoutProclamationsInput
userProclamations?: Prisma.UserProclamationsCreateNestedManyWithoutProclamationInput
@@ -764,7 +718,6 @@ export type ProclamationUncheckedCreateWithoutSubscriptionLevelInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
userProclamations?: Prisma.UserProclamationsUncheckedCreateNestedManyWithoutProclamationInput
}
@@ -799,7 +752,6 @@ export type ProclamationCreateManySolfeggioFrequencyInput = {
name: string
content: string
categoryId: number
price?: number
subscriptionLevelId?: number
}
@@ -807,7 +759,6 @@ export type ProclamationUpdateWithoutSolfeggioFrequencyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -818,7 +769,6 @@ export type ProclamationUncheckedUpdateWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -828,7 +778,6 @@ export type ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -837,7 +786,6 @@ export type ProclamationCreateManyCategoryInput = {
name: string
content: string
solfeggioFrequencyId: number
price?: number
subscriptionLevelId?: number
}
@@ -845,7 +793,6 @@ export type ProclamationUpdateWithoutCategoryInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
subscriptionLevel?: Prisma.SubscriptionLevelUpdateOneRequiredWithoutProclamationsNestedInput
@@ -856,7 +803,6 @@ export type ProclamationUncheckedUpdateWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -866,7 +812,6 @@ export type ProclamationUncheckedUpdateManyWithoutCategoryInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
subscriptionLevelId?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -876,14 +821,12 @@ export type ProclamationCreateManySubscriptionLevelInput = {
content: string
solfeggioFrequencyId: number
categoryId: number
price?: number
}
export type ProclamationUpdateWithoutSubscriptionLevelInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
content?: Prisma.StringFieldUpdateOperationsInput | string
price?: Prisma.FloatFieldUpdateOperationsInput | number
solfeggioFrequency?: Prisma.SolfeggioFrequencyUpdateOneRequiredWithoutProclamationsNestedInput
category?: Prisma.ProclamationCategoryUpdateOneRequiredWithoutProclamationsNestedInput
userProclamations?: Prisma.UserProclamationsUpdateManyWithoutProclamationNestedInput
@@ -895,7 +838,6 @@ export type ProclamationUncheckedUpdateWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
userProclamations?: Prisma.UserProclamationsUncheckedUpdateManyWithoutProclamationNestedInput
}
@@ -905,7 +847,6 @@ export type ProclamationUncheckedUpdateManyWithoutSubscriptionLevelInput = {
content?: Prisma.StringFieldUpdateOperationsInput | string
solfeggioFrequencyId?: Prisma.IntFieldUpdateOperationsInput | number
categoryId?: Prisma.IntFieldUpdateOperationsInput | number
price?: Prisma.FloatFieldUpdateOperationsInput | number
}
@@ -945,7 +886,6 @@ export type ProclamationSelect<ExtArgs extends runtime.Types.Extensions.Internal
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -960,7 +900,6 @@ export type ProclamationSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -973,7 +912,6 @@ export type ProclamationSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -986,11 +924,10 @@ export type ProclamationSelectScalar = {
content?: boolean
solfeggioFrequencyId?: boolean
categoryId?: boolean
price?: boolean
subscriptionLevelId?: boolean
}
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "price" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
export type ProclamationOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "content" | "solfeggioFrequencyId" | "categoryId" | "subscriptionLevelId", ExtArgs["result"]["proclamation"]>
export type ProclamationInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
solfeggioFrequency?: boolean | Prisma.SolfeggioFrequencyDefaultArgs<ExtArgs>
category?: boolean | Prisma.ProclamationCategoryDefaultArgs<ExtArgs>
@@ -1023,7 +960,6 @@ export type $ProclamationPayload<ExtArgs extends runtime.Types.Extensions.Intern
content: string
solfeggioFrequencyId: number
categoryId: number
price: number
subscriptionLevelId: number
}, ExtArgs["result"]["proclamation"]>
composites: {}
@@ -1457,7 +1393,6 @@ export interface ProclamationFieldRefs {
readonly content: Prisma.FieldRef<"Proclamation", 'String'>
readonly solfeggioFrequencyId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly categoryId: Prisma.FieldRef<"Proclamation", 'Int'>
readonly price: Prisma.FieldRef<"Proclamation", 'Float'>
readonly subscriptionLevelId: Prisma.FieldRef<"Proclamation", 'Int'>
}
@@ -1655,6 +1590,11 @@ export type ProclamationFindManyArgs<ExtArgs extends runtime.Types.Extensions.In
* Skip the first `n` Proclamations.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Proclamations.
*/
distinct?: Prisma.ProclamationScalarFieldEnum | Prisma.ProclamationScalarFieldEnum[]
}
@@ -182,7 +182,7 @@ export type ProclamationCategoryGroupByOutputType = {
_max: ProclamationCategoryMaxAggregateOutputType | null
}
type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise<
export type GetProclamationCategoryGroupByPayload<T extends ProclamationCategoryGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProclamationCategoryGroupByOutputType, T['by']> &
{
@@ -1297,6 +1297,11 @@ export type ProclamationCategoryFindManyArgs<ExtArgs extends runtime.Types.Exten
* Skip the first `n` ProclamationCategories.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of ProclamationCategories.
*/
distinct?: Prisma.ProclamationCategoryScalarFieldEnum | Prisma.ProclamationCategoryScalarFieldEnum[]
}
+43 -2
View File
@@ -30,6 +30,7 @@ export type ProfileMinAggregateOutputType = {
firstName: string | null
lastName: string | null
phoneNumber: string | null
ghlContactId: string | null
}
export type ProfileMaxAggregateOutputType = {
@@ -38,6 +39,7 @@ export type ProfileMaxAggregateOutputType = {
firstName: string | null
lastName: string | null
phoneNumber: string | null
ghlContactId: string | null
}
export type ProfileCountAggregateOutputType = {
@@ -46,6 +48,7 @@ export type ProfileCountAggregateOutputType = {
firstName: number
lastName: number
phoneNumber: number
ghlContactId: number
_all: number
}
@@ -56,6 +59,7 @@ export type ProfileMinAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
}
export type ProfileMaxAggregateInputType = {
@@ -64,6 +68,7 @@ export type ProfileMaxAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
}
export type ProfileCountAggregateInputType = {
@@ -72,6 +77,7 @@ export type ProfileCountAggregateInputType = {
firstName?: true
lastName?: true
phoneNumber?: true
ghlContactId?: true
_all?: true
}
@@ -153,12 +159,13 @@ export type ProfileGroupByOutputType = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId: string | null
_count: ProfileCountAggregateOutputType | null
_min: ProfileMinAggregateOutputType | null
_max: ProfileMaxAggregateOutputType | null
}
type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise<
export type GetProfileGroupByPayload<T extends ProfileGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<ProfileGroupByOutputType, T['by']> &
{
@@ -182,6 +189,7 @@ export type ProfileWhereInput = {
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter
}
@@ -192,6 +200,7 @@ export type ProfileOrderByWithRelationInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
user?: Prisma.UserOrderByWithRelationInput
addresses?: Prisma.AddressOrderByRelationAggregateInput
}
@@ -205,6 +214,7 @@ export type ProfileWhereUniqueInput = Prisma.AtLeast<{
firstName?: Prisma.StringFilter<"Profile"> | string
lastName?: Prisma.StringFilter<"Profile"> | string
phoneNumber?: Prisma.StringFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableFilter<"Profile"> | string | null
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
addresses?: Prisma.AddressListRelationFilter
}, "id" | "userId">
@@ -215,6 +225,7 @@ export type ProfileOrderByWithAggregationInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrderInput | Prisma.SortOrder
_count?: Prisma.ProfileCountOrderByAggregateInput
_max?: Prisma.ProfileMaxOrderByAggregateInput
_min?: Prisma.ProfileMinOrderByAggregateInput
@@ -229,6 +240,7 @@ export type ProfileScalarWhereWithAggregatesInput = {
firstName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
lastName?: Prisma.StringWithAggregatesFilter<"Profile"> | string
phoneNumber?: Prisma.StringWithAggregatesFilter<"Profile"> | string
ghlContactId?: Prisma.StringNullableWithAggregatesFilter<"Profile"> | string | null
}
export type ProfileCreateInput = {
@@ -236,6 +248,7 @@ export type ProfileCreateInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
}
@@ -246,6 +259,7 @@ export type ProfileUncheckedCreateInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
}
@@ -254,6 +268,7 @@ export type ProfileUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
@@ -264,6 +279,7 @@ export type ProfileUncheckedUpdateInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
}
@@ -273,6 +289,7 @@ export type ProfileCreateManyInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
}
export type ProfileUpdateManyMutationInput = {
@@ -280,6 +297,7 @@ export type ProfileUpdateManyMutationInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileUncheckedUpdateManyInput = {
@@ -288,6 +306,7 @@ export type ProfileUncheckedUpdateManyInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
export type ProfileNullableScalarRelationFilter = {
@@ -301,6 +320,7 @@ export type ProfileCountOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileMaxOrderByAggregateInput = {
@@ -309,6 +329,7 @@ export type ProfileMaxOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileMinOrderByAggregateInput = {
@@ -317,6 +338,7 @@ export type ProfileMinOrderByAggregateInput = {
firstName?: Prisma.SortOrder
lastName?: Prisma.SortOrder
phoneNumber?: Prisma.SortOrder
ghlContactId?: Prisma.SortOrder
}
export type ProfileScalarRelationFilter = {
@@ -375,6 +397,7 @@ export type ProfileCreateWithoutUserInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressCreateNestedManyWithoutProfileInput
}
@@ -383,6 +406,7 @@ export type ProfileUncheckedCreateWithoutUserInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
addresses?: Prisma.AddressUncheckedCreateNestedManyWithoutProfileInput
}
@@ -407,6 +431,7 @@ export type ProfileUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUpdateManyWithoutProfileNestedInput
}
@@ -415,6 +440,7 @@ export type ProfileUncheckedUpdateWithoutUserInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
addresses?: Prisma.AddressUncheckedUpdateManyWithoutProfileNestedInput
}
@@ -423,6 +449,7 @@ export type ProfileCreateWithoutAddressesInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
user: Prisma.UserCreateNestedOneWithoutProfileInput
}
@@ -432,6 +459,7 @@ export type ProfileUncheckedCreateWithoutAddressesInput = {
firstName: string
lastName: string
phoneNumber: string
ghlContactId?: string | null
}
export type ProfileCreateOrConnectWithoutAddressesInput = {
@@ -455,6 +483,7 @@ export type ProfileUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
user?: Prisma.UserUpdateOneRequiredWithoutProfileNestedInput
}
@@ -464,6 +493,7 @@ export type ProfileUncheckedUpdateWithoutAddressesInput = {
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
phoneNumber?: Prisma.StringFieldUpdateOperationsInput | string
ghlContactId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
}
@@ -503,6 +533,7 @@ export type ProfileSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
_count?: boolean | Prisma.ProfileCountOutputTypeDefaultArgs<ExtArgs>
@@ -514,6 +545,7 @@ export type ProfileSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
@@ -523,6 +555,7 @@ export type ProfileSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Exten
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
}, ExtArgs["result"]["profile"]>
@@ -532,9 +565,10 @@ export type ProfileSelectScalar = {
firstName?: boolean
lastName?: boolean
phoneNumber?: boolean
ghlContactId?: boolean
}
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber", ExtArgs["result"]["profile"]>
export type ProfileOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "firstName" | "lastName" | "phoneNumber" | "ghlContactId", ExtArgs["result"]["profile"]>
export type ProfileInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
addresses?: boolean | Prisma.Profile$addressesArgs<ExtArgs>
@@ -559,6 +593,7 @@ export type $ProfilePayload<ExtArgs extends runtime.Types.Extensions.InternalArg
firstName: string
lastName: string
phoneNumber: string
ghlContactId: string | null
}, ExtArgs["result"]["profile"]>
composites: {}
}
@@ -989,6 +1024,7 @@ export interface ProfileFieldRefs {
readonly firstName: Prisma.FieldRef<"Profile", 'String'>
readonly lastName: Prisma.FieldRef<"Profile", 'String'>
readonly phoneNumber: Prisma.FieldRef<"Profile", 'String'>
readonly ghlContactId: Prisma.FieldRef<"Profile", 'String'>
}
@@ -1185,6 +1221,11 @@ export type ProfileFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Profiles.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Profiles.
*/
distinct?: Prisma.ProfileScalarFieldEnum | Prisma.ProfileScalarFieldEnum[]
}
+6 -1
View File
@@ -179,7 +179,7 @@ export type SessionGroupByOutputType = {
_max: SessionMaxAggregateOutputType | null
}
type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise<
export type GetSessionGroupByPayload<T extends SessionGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SessionGroupByOutputType, T['by']> &
{
@@ -1227,6 +1227,11 @@ export type SessionFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Skip the first `n` Sessions.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Sessions.
*/
distinct?: Prisma.SessionScalarFieldEnum | Prisma.SessionScalarFieldEnum[]
}
@@ -182,7 +182,7 @@ export type SolfeggioFrequencyGroupByOutputType = {
_max: SolfeggioFrequencyMaxAggregateOutputType | null
}
type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise<
export type GetSolfeggioFrequencyGroupByPayload<T extends SolfeggioFrequencyGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SolfeggioFrequencyGroupByOutputType, T['by']> &
{
@@ -203,7 +203,7 @@ export type SolfeggioFrequencyWhereInput = {
NOT?: Prisma.SolfeggioFrequencyWhereInput | Prisma.SolfeggioFrequencyWhereInput[]
id?: Prisma.IntFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatFilter<"SolfeggioFrequency"> | number
value?: Prisma.IntFilter<"SolfeggioFrequency"> | number
proclamations?: Prisma.ProclamationListRelationFilter
}
@@ -241,7 +241,7 @@ export type SolfeggioFrequencyScalarWhereWithAggregatesInput = {
NOT?: Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput | Prisma.SolfeggioFrequencyScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
name?: Prisma.StringWithAggregatesFilter<"SolfeggioFrequency"> | string
value?: Prisma.FloatWithAggregatesFilter<"SolfeggioFrequency"> | number
value?: Prisma.IntWithAggregatesFilter<"SolfeggioFrequency"> | number
}
export type SolfeggioFrequencyCreateInput = {
@@ -259,14 +259,14 @@ export type SolfeggioFrequencyUncheckedCreateInput = {
export type SolfeggioFrequencyUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUpdateManyWithoutSolfeggioFrequencyNestedInput
}
export type SolfeggioFrequencyUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
proclamations?: Prisma.ProclamationUncheckedUpdateManyWithoutSolfeggioFrequencyNestedInput
}
@@ -278,13 +278,13 @@ export type SolfeggioFrequencyCreateManyInput = {
export type SolfeggioFrequencyUpdateManyMutationInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyScalarRelationFilter = {
@@ -363,13 +363,13 @@ export type SolfeggioFrequencyUpdateToOneWithWhereWithoutProclamationsInput = {
export type SolfeggioFrequencyUpdateWithoutProclamationsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
export type SolfeggioFrequencyUncheckedUpdateWithoutProclamationsInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
name?: Prisma.StringFieldUpdateOperationsInput | string
value?: Prisma.FloatFieldUpdateOperationsInput | number
value?: Prisma.IntFieldUpdateOperationsInput | number
}
@@ -872,7 +872,7 @@ export interface Prisma__SolfeggioFrequencyClient<T, Null = never, ExtArgs exten
export interface SolfeggioFrequencyFieldRefs {
readonly id: Prisma.FieldRef<"SolfeggioFrequency", 'Int'>
readonly name: Prisma.FieldRef<"SolfeggioFrequency", 'String'>
readonly value: Prisma.FieldRef<"SolfeggioFrequency", 'Float'>
readonly value: Prisma.FieldRef<"SolfeggioFrequency", 'Int'>
}
@@ -1069,6 +1069,11 @@ export type SolfeggioFrequencyFindManyArgs<ExtArgs extends runtime.Types.Extensi
* Skip the first `n` SolfeggioFrequencies.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of SolfeggioFrequencies.
*/
distinct?: Prisma.SolfeggioFrequencyScalarFieldEnum | Prisma.SolfeggioFrequencyScalarFieldEnum[]
}
@@ -178,7 +178,7 @@ export type SubscriptionLevelGroupByOutputType = {
_max: SubscriptionLevelMaxAggregateOutputType | null
}
type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise<
export type GetSubscriptionLevelGroupByPayload<T extends SubscriptionLevelGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<SubscriptionLevelGroupByOutputType, T['by']> &
{
@@ -1063,6 +1063,11 @@ export type SubscriptionLevelFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` SubscriptionLevels.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of SubscriptionLevels.
*/
distinct?: Prisma.SubscriptionLevelScalarFieldEnum | Prisma.SubscriptionLevelScalarFieldEnum[]
}
+6 -1
View File
@@ -213,7 +213,7 @@ export type UserGroupByOutputType = {
_max: UserMaxAggregateOutputType | null
}
type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise<
export type GetUserGroupByPayload<T extends UserGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserGroupByOutputType, T['by']> &
{
@@ -1883,6 +1883,11 @@ export type UserFindManyArgs<ExtArgs extends runtime.Types.Extensions.InternalAr
* Skip the first `n` Users.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Users.
*/
distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[]
}
@@ -151,7 +151,7 @@ export type UserProclamationsGroupByOutputType = {
_max: UserProclamationsMaxAggregateOutputType | null
}
type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise<
export type GetUserProclamationsGroupByPayload<T extends UserProclamationsGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserProclamationsGroupByOutputType, T['by']> &
{
@@ -1207,6 +1207,11 @@ export type UserProclamationsFindManyArgs<ExtArgs extends runtime.Types.Extensio
* Skip the first `n` UserProclamations.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of UserProclamations.
*/
distinct?: Prisma.UserProclamationsScalarFieldEnum | Prisma.UserProclamationsScalarFieldEnum[]
}
+6 -1
View File
@@ -171,7 +171,7 @@ export type UserRoleGroupByOutputType = {
_max: UserRoleMaxAggregateOutputType | null
}
type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise<
export type GetUserRoleGroupByPayload<T extends UserRoleGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<UserRoleGroupByOutputType, T['by']> &
{
@@ -1041,6 +1041,11 @@ export type UserRoleFindManyArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Skip the first `n` UserRoles.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of UserRoles.
*/
distinct?: Prisma.UserRoleScalarFieldEnum | Prisma.UserRoleScalarFieldEnum[]
}
@@ -165,7 +165,7 @@ export type VerificationGroupByOutputType = {
_max: VerificationMaxAggregateOutputType | null
}
type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise<
export type GetVerificationGroupByPayload<T extends VerificationGroupByArgs> = Prisma.PrismaPromise<
Array<
Prisma.PickEnumerable<VerificationGroupByOutputType, T['by']> &
{
@@ -982,6 +982,11 @@ export type VerificationFindManyArgs<ExtArgs extends runtime.Types.Extensions.In
* Skip the first `n` Verifications.
*/
skip?: number
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs}
*
* Filter by unique combinations of Verifications.
*/
distinct?: Prisma.VerificationScalarFieldEnum | Prisma.VerificationScalarFieldEnum[]
}
+52 -48
View File
@@ -1,71 +1,75 @@
// Contains all payment related functions such as making payments,
// issueing refunds, storing and deleting credit cards, and more.
// A full list of implementation requirements will be kept below.
//
// issueing refunds, storing and deleting credit cards, and more.
// A full list of implementation requirements will be kept below.
//
// ---- Implementations ----
// ✔ Make a payment...
// ✔ With a non-saved card
// ✔ With a saved card
// ✔ Add a card
// ✔ Delete a card
// ✔ Delete a card
// ✔ Issue a refund
import type { Decimal } from "@prisma/client/runtime/client";
import type { Decimal } from '@prisma/client/runtime/client';
export interface CreditDebitCard {
first_name: string,
last_name: string,
card_number: number,
expiration_month: number,
expiration_year: number,
security_code: number,
first_name: string;
last_name: string;
card_number: number;
expiration_month: number;
expiration_year: number;
security_code: number;
}
export async function chargeCreditDebitCard (card: CreditDebitCard, amount: Decimal): Promise<null> {
/**
* chargeCreditDebitCard attempts to request payment from a valid card,
* for the amount specified.
*/
console.log(`charged card ${card} for $${amount}`);
await new Promise(resolve => setTimeout(resolve, 1000));
return null;
export async function chargeCreditDebitCard(card: CreditDebitCard, amount: Decimal): Promise<null> {
/**
* chargeCreditDebitCard attempts to request payment from a valid card,
* for the amount specified.
*/
console.log(`charged card ${card} for $${amount}`);
await new Promise((resolve) => setTimeout(resolve, 1000));
return null;
}
export async function chargedStoredCard(cardNumber: number, profileID: string, amount: Decimal): Promise<null> {
/**
* chargedStoredCard attempts to request payment from a valid card,
* for the amount specified.
* The card is identified by the last four digits of the card
* number, and the profile ID.
* NOTE: This should be enough to identify a saved card, but the
* function signature may change in the future.
*/
console.log(`charged ${profileID}'s card ${cardNumber} for ${cardNumber} $${amount}`);
return null;
export async function chargedStoredCard(
cardNumber: number,
profileID: string,
amount: Decimal
): Promise<null> {
/**
* chargedStoredCard attempts to request payment from a valid card,
* for the amount specified.
* The card is identified by the last four digits of the card
* number, and the profile ID.
* NOTE: This should be enough to identify a saved card, but the
* function signature may change in the future.
*/
console.log(`charged ${profileID}'s card ${cardNumber} for ${cardNumber} $${amount}`);
return null;
}
export async function deleteCardFromProfile(cardNumber: number, profileID: string): Promise<null> {
/**
* deleteCardFromProfile deletes a stored card on a user's profile.
*/
console.log(`Deleted card ${cardNumber} from ${profileID}'s profile.`);
return null;
/**
* deleteCardFromProfile deletes a stored card on a user's profile.
*/
console.log(`Deleted card ${cardNumber} from ${profileID}'s profile.`);
return null;
}
export async function storeCardToProfile(card: CreditDebitCard, profileID: string): Promise<null> {
/**
* storeCardToProfile stores a new card to a user's profile
* for easy re-use later.
*/
console.log(`Deleted card ${card.card_number} from ${profileID}'s profile.`);
return null;
/**
* storeCardToProfile stores a new card to a user's profile
* for easy re-use later.
*/
console.log(`Deleted card ${card.card_number} from ${profileID}'s profile.`);
return null;
}
export async function requestRefund(card: CreditDebitCard, amount: Decimal): Promise<null> {
/**
* requestRefund attempts to process a refund for the amount to the
* provided card.
*/
console.log(`Refunded ${amount} to ${card.card_number}`);
return null;
}
/**
* requestRefund attempts to process a refund for the amount to the
* provided card.
*/
console.log(`Refunded ${amount} to ${card.card_number}`);
return null;
}
+1 -4
View File
@@ -1,10 +1,7 @@
import { BETTER_AUTH_URL } from '$env/static/private';
import { sendMail } from './resend';
export async function sendVerificationEmail(options: {
email: string;
token: string;
}) {
export async function sendVerificationEmail(options: { email: string; token: string }) {
const verificationUrl = `${BETTER_AUTH_URL}/api/auth/verify-email?token=${options.token}&callbackURL=/login?verified=true`;
await sendMail({
+21 -21
View File
@@ -2,11 +2,11 @@
* AUTHOR: Neil Clack
* EMAIL: nclack@radiantwavetech.com
* DATE: 07/06/2027
*
*
* A server side library and client for Resend
* Includes the Resend client, as well as functions
* for different email scenarios.
*
*
*/
import { Resend } from 'resend';
import { RESEND_API_KEY, EMAIL_FROM, RESEND_TEMPLATE_ID } from '$env/static/private';
@@ -31,26 +31,26 @@ export const resend = new Resend(RESEND_API_KEY);
* @throws {Error} If the Resend API call fails.
*/
export async function sendMail(options: {
to: string;
subject?: string;
templateId?: string;
data?: Record<string, string | number>;
to: string;
subject?: string;
templateId?: string;
data?: Record<string, string | number>;
}) {
const { to, subject, templateId, data } = options;
const { to, subject, templateId, data } = options;
const result = await resend.emails.send({
from: EMAIL_FROM,
to: [to],
subject: subject ?? 'Template Email',
template: {
id: templateId ?? RESEND_TEMPLATE_ID,
variables: data ?? {}
}
});
const result = await resend.emails.send({
from: EMAIL_FROM,
to: [to],
subject: subject ?? 'Template Email',
template: {
id: templateId ?? RESEND_TEMPLATE_ID,
variables: data ?? {}
}
});
if (result.error) {
throw new Error(result.error.message);
}
if (result.error) {
throw new Error(result.error.message);
}
return result.data;
}
return result.data;
}
+102 -81
View File
@@ -1,8 +1,8 @@
# svelte-admin
A config-driven admin panel framework for SvelteKit + Prisma. Define your models once in a
registry and get full CRUD UI with dynamic routing, role-based access control, searchable
dropdowns, and related-record panels — no per-model route files needed.
registry and get full CRUD UI with per-model static pages, role-based access control, searchable
tables, FK dropdowns, TipTap editor for rich text, and related-record panels.
## Architecture
@@ -13,27 +13,30 @@ src/lib/svelte-admin/ # Framework core
├── roles.ts # Role hierarchy and access helpers
├── prisma-delegate.ts # Dynamic Prisma delegate lookup
├── introspect.ts # Field label generation and dot-notation resolution
├── form-data.ts # FormData → typed object parser
├── form-data.ts # FormData -> typed object parser
├── crud.ts # CRUD action factory (load, upsert, delete)
└── index.ts # Barrel exports
src/routes/(protected)/admin/ # Dynamic admin routes
src/routes/(protected)/admin/ # Per-model admin pages (no dynamic [model] routing)
├── +layout.server.ts # Role-based access guard
├── +layout.svelte # Layout shell
├── +page.server.ts # Admin home (model grid)
├── +page.svelte # Admin home UI
├── [model]/
│ ├── +page.server.ts # List view + create action
│ ├── +page.svelte # List view UI
│ └── [id]/
│ ├── +page.server.ts # Edit view + update/delete actions
│ └── +page.svelte # Edit view UI
├── users/ # Each model gets its own page directory
├── profiles/
├── addresses/
├── user-roles/
├── proclamations/
├── solfeggio-frequencies/
├── proclamation-categories/
├── subscription-levels/
└── license-keys/ # listOnly — no create/edit modal
src/lib/components/admin/ # Reusable admin UI components
├── AdminTable.svelte # Sortable, searchable data table
├── AdminForm.svelte # Dynamic form with type-aware inputs
├── ComboBox.svelte # Searchable dropdown for FK fields
├── RelatedHasOne.svelte # Display panel for 1:1 related records
└── RelatedHasMany.svelte # Display table for 1:N related records
├── AdminModal.svelte # Modal overlay for CRUD forms
├── TipTap.svelte # Rich-text editor (in components/tiptap/)
```
## Adding a New Model
@@ -74,10 +77,11 @@ src/lib/components/admin/ # Reusable admin UI components
}
```
3. **Done.** Visit `/admin/widgets`. The dynamic `[model]` routes handle list, create, edit,
and delete automatically.
3. **Create the page directory** `src/routes/(protected)/admin/widgets/` with:
- `+page.server.ts` — load records + form actions using `crud.ts` helpers
- `+page.svelte` — render AdminTable + AdminModal with AdminForm
No new route files, no new components. The registry is the only thing you touch.
4. **Done.** Visit `/admin/widgets`. The page handles list, create, edit via modal.
## Configuration Reference
@@ -95,25 +99,26 @@ No new route files, no new components. The registry is the only thing you touch.
| `searchFields` | `string[]` | Yes | Fields searched by the client-side search filter. |
| `formFields` | `string[]` | Yes | Fields rendered in create/edit forms, in display order. |
| `readonlyFields` | `string[]` | No | Fields shown as read-only on the edit page. |
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table. |
| `listOnly` | `boolean` | No | If `true`, suppresses edit links in the table and no modal. |
| `fieldTypes` | `Record<string, PrismaScalarType>` | Yes | Maps field names to scalar types. Controls input rendering and FormData parsing. |
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as ComboBox dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages. |
| `relationOptions` | `Record<string, RelationOptionConfig>` | No | FK fields that render as `<select>` dropdowns. |
| `relatedRecords` | `RelatedRecordConfig[]` | No | Has-one/has-many panels shown on edit pages (display only via related records table). |
| `accessFilter` | `(viewerRoleId: number) => where \| undefined` | No | Returns a Prisma `where` clause to filter records by viewer role. `undefined` = no filter. |
### PrismaScalarType
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime'`
`'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Tiptap'`
Controls both form input rendering and FormData parsing:
| Type | HTML Input | Parse Logic |
| ---------- | ----------------------------------- | ------------------------------ |
| `String` | `<input type="text">` | `raw.toString()` |
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty → `null` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty → `null` |
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` → `true` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty → `null` |
| Type | HTML Input | Parse Logic |
| ---------- | ------------------------------------- | ------------------------------ |
| `String` | `<input type="text">` | `raw.toString()` |
| `Int` | `<input type="number" step="1">` | `parseInt()`, empty -> `null` |
| `Float` | `<input type="number" step="0.01">` | `parseFloat()`, empty -> `null` |
| `Boolean` | `<input type="checkbox">` | `'on'` or `'true'` -> `true` |
| `DateTime` | `<input type="datetime-local">` | `new Date()`, empty -> `null` |
| `Tiptap` | TipTap rich-text editor (hidden input)| `raw.toString()` |
### RelationOptionConfig
@@ -142,14 +147,18 @@ Controls both form input rendering and FormData parsing:
The `fieldTypes` mapping is the central mechanism connecting Prisma scalar types to HTML inputs
and form data parsing. Every field in `formFields` must have a corresponding entry in `fieldTypes`.
When a field also has a `relationOptions` entry, the framework renders a `ComboBox` (searchable
dropdown) instead of the default input for that type.
When a field also has a `relationOptions` entry, the framework renders a `<select>` dropdown
instead of the default input for that type. The options are fetched server-side by `crud.ts`
and passed to the client as `relationOptionsData`.
For `Tiptap` fields, the TipTap rich-text editor is rendered. The editor content is stored in a
hidden input and submitted with the form.
## Relations
### Foreign Key Dropdowns (relationOptions)
When a form field is a foreign key, add a `relationOptions` entry to render a searchable ComboBox:
When a form field is a foreign key, add a `relationOptions` entry to render a `<select>` dropdown:
```ts
{
@@ -165,7 +174,8 @@ When a form field is a foreign key, add a `relationOptions` entry to render a se
}
```
The route loader automatically queries the related model and passes options to the ComboBox.
The `crud.ts` `loadRecords` function automatically queries the related model and passes options
to the page as `relationOptionsData`.
### Eager Loading (include)
@@ -200,9 +210,7 @@ relatedRecords: [
];
```
The `relationKey` must match a key in `include` so the data is actually loaded. `has-one` renders
a key-value panel via `RelatedHasOne`; `has-many` renders a table via `RelatedHasMany`. Both
support an optional `editSlug` to link to the related record's edit page.
The `relationKey` must match a key in `include` so the data is actually loaded.
## Access Control
@@ -228,23 +236,23 @@ Use `accessFilter` to restrict which records a viewer can see:
```ts
accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= ROLES.SUPER) return undefined; // no filter
if (viewerRoleId <= ROLES.ADMIN) {
return { roleId: { gte: ROLES.STAFF } }; // can't see other admins
}
return { roleId: { gt: ROLES.STAFF } }; // staff sees only regular users
if (viewerRoleId <= ROLES.SUPER) return undefined;
if (viewerRoleId <= ROLES.ADMIN) {
return { roleId: { gte: ROLES.STAFF } };
}
return { roleId: { gt: ROLES.STAFF } };
};
```
## How Dynamic Routing Works
## How Static Pages Work
1. User visits `/admin/proclamations` (or `/admin/proclamations/abc123`)
2. SvelteKit matches the `[model]` (and `[id]`) route params
3. `+page.server.ts` calls `getModelConfig(params.model)` to look up the config
4. `getDelegate(config)` returns `prisma[lcfirst(config.modelName)]` (e.g., `prisma.proclamation`)
5. The delegate runs `findMany` (list), `findUnique` (edit), `create`, `update`, or `delete`
6. Data + config are passed to the Svelte page component
7. Components (`AdminTable`, `AdminForm`, etc.) render based on config
1. User visits `/admin/proclamations`
2. SvelteKit matches the static route `/admin/proclamations/+page.server.ts`
3. `+page.server.ts` calls `getModelConfig('proclamations')` to look up the config
4. `loadRecords(config)` calls `prisma.proclamation.findMany()` with the config's `include` and `accessFilter`
5. For each FK field with a `relationOptions`, it also queries the related model for dropdown options
6. Data + config are passed to `+page.svelte`
7. The page renders `AdminTable` with records and `AdminModal` + `AdminForm` for create/edit
## Component API
@@ -255,11 +263,13 @@ accessFilter: (viewerRoleId: number) => {
| `records` | `Record<string, unknown>[]` | Data rows |
| `listFields` | `string[]` | Column keys (dot-notation supported) |
| `searchFields` | `string[]` | Fields for client-side search |
| `modelSlug` | `string` | Used for building edit links |
| `listOnly` | `boolean?` | Suppress edit links |
| `fieldTypes` | `Record<string, PrismaScalarType>` | Type mapping for cell formatting |
| `singular` | `string` | Singular label for UI text |
| `onRowClick` | `(record) => void?` | Click handler for edit modal |
| `onNew` | `() => void?` | Click handler for create modal |
Features: client-side search, click-to-sort columns, auto-formatted cells (dates, booleans,
decimals), filtered count display.
decimals), empty state.
### AdminForm
@@ -270,46 +280,31 @@ decimals), filtered count display.
| `relationOptions` | `Record<string, RelationOptionConfig>?` | FK dropdown configs |
| `relationOptionsData` | `Record<string, { label; value }[]>?` | Loaded dropdown options |
| `record` | `Record<string, unknown>?` | Pre-fill values (edit mode) |
| `form` | `ActionData?` | Carries validation errors |
| `action` | `string` | Form action (e.g., `'?/create'`) |
| `readonlyFields` | `string[]?` | Read-only field names |
| `action` | `string` | Form action (e.g., `'?/upsert'`) |
| `submitLabel` | `string?` | Button text |
### ComboBox
### AdminModal
| Prop | Type | Description |
| ------------- | -------------------- | ------------------ |
| `id` | `string` | Element ID |
| `name` | `string` | Form field name |
| `options` | `{ label; value }[]` | Dropdown items |
| `placeholder` | `string?` | Placeholder text |
| `value` | `string \| number?` | Pre-selected value |
| Prop | Type | Description |
| --------- | ----------------- | ------------------------ |
| `open` | `boolean` | Show/hide modal |
| `title` | `string` | Modal heading |
| `onClose` | `() => void` | Close handler |
| `children`| `Snippet` | Modal body content |
Features: type-to-search filtering, keyboard navigation (ArrowUp/Down, Enter, Escape),
clear button, scroll-to-highlight.
### TipTap
### RelatedHasOne
| Prop | Type | Description |
| --------------- | --------------------------------- | ------------------------------------ |
| `label` | `string` | Section heading |
| `record` | `Record<string, unknown> \| null` | Related record |
| `displayFields` | `string[]` | Fields to display as key-value pairs |
| `editSlug` | `string?` | Links to edit the related record |
### RelatedHasMany
| Prop | Type | Description |
| --------------- | --------------------------- | ----------------------------- |
| `label` | `string` | Section heading (shows count) |
| `records` | `Record<string, unknown>[]` | Related records |
| `displayFields` | `string[]` | Table columns |
| `editSlug` | `string?` | Per-row edit links |
| Prop | Type | Description |
| ---------------- | -------- | --------------------------------- |
| `initialContent` | `string` | Initial HTML content |
| `name` | `string` | Hidden input name for form submit |
## Utility Functions
| Function | Module | Description |
| ------------------------------------------ | -------------------- | ----------------------------------------------------------------------------- |
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` → `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
| `fieldNameToLabel(name)` | `introspect.ts` | `'solfeggioFrequencyId'` -> `'Solfeggio Frequency'` (strips `Id`, adds spaces) |
| `resolveFieldValue(record, path)` | `introspect.ts` | Resolves dot-notation paths like `'category.name'` on nested objects |
| `extractFormData(formData, fields, types)` | `form-data.ts` | Parses `FormData` with type-aware conversion per `PrismaScalarType` |
| `getDelegate(config)` | `prisma-delegate.ts` | Returns `prisma[lcfirst(modelName)]` delegate |
@@ -318,6 +313,32 @@ clear button, scroll-to-highlight.
| `getAllModelConfigs()` | `registry.ts` | Returns all registered model configs |
| `hasRole(roleId, required)` | `roles.ts` | `true` if `roleId <= required` |
| `canAccessAdmin(roleId)` | `roles.ts` | `true` if `roleId <= ROLES.STAFF` |
| `loadRecords(locals, config)` | `crud.ts` | Fetches records + FK dropdown options for a model |
| `upsertRecord(formData, config, locals)` | `crud.ts` | Creates or updates a record based on presence of `id` in form data |
| `deleteRecord(formData, config, locals)` | `crud.ts` | Deletes a record by ID |
## Models in Admin
| Registry Slug | Prisma Model | Notes |
| -------------------------- | ---------------------- | ---------------------------------- |
| `users` | User | Role assignment, view-only fields |
| `profiles` | Profile | Linked to user FK |
| `addresses` | Address | Linked to profile FK |
| `user-roles` | UserRole | |
| `proclamations` | Proclamation | TipTap rich-text editor for content|
| `solfeggio-frequencies` | SolfeggioFrequency | |
| `proclamation-categories` | ProclamationCategory | Parent category hierarchy |
| `license-keys` | LicenseKey | listOnly — read-only table, no CRUD|
| `subscription-levels` | SubscriptionLevel | |
### Excluded from Admin
| Model | Reason |
| ------------------ | -------------------------------------------- |
| Session | Better Auth internal — not for manual editing|
| Account | Better Auth internal |
| Verification | Better Auth internal |
| UserProclamations | Composite key — managed via other models |
## Complete Example
@@ -364,7 +385,7 @@ A hypothetical `Product` model showing all configuration features:
],
accessFilter: (viewerRoleId: number) => {
if (viewerRoleId <= 2) return undefined;
return { active: true }; // staff only sees active products
return { active: true };
}
}
```
+124
View File
@@ -0,0 +1,124 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createLocals, mockUser } from '$lib/test-utils';
import { getModelConfig } from './registry';
vi.mock('$lib/db', () => ({
prisma: {
userRole: {
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn()
}
}
}));
import { prisma } from '$lib/db';
import { loadRecords, upsertRecord, deleteRecord } from './crud';
const findMany = prisma.userRole.findMany as ReturnType<typeof vi.fn>;
const create = prisma.userRole.create as ReturnType<typeof vi.fn>;
const update = prisma.userRole.update as ReturnType<typeof vi.fn>;
const deletes = prisma.userRole.delete as ReturnType<typeof vi.fn>;
const findUnique = prisma.userRole.findUnique as ReturnType<typeof vi.fn>;
const findFirst = prisma.userRole.findFirst as ReturnType<typeof vi.fn>;
const config = getModelConfig('user-roles')!;
describe('crud loadRecords', () => {
beforeEach(() => {
findMany.mockReset();
});
it('returns records', async () => {
findMany.mockResolvedValue([{ id: 1, name: 'Admin' }]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([{ id: 1, name: 'Admin' }]);
});
it('returns empty records array', async () => {
findMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([]);
});
});
describe('crud upsertRecord', () => {
beforeEach(() => {
create.mockReset();
update.mockReset();
findUnique.mockReset();
findFirst.mockReset();
});
it('creates a new record when no id', async () => {
create.mockResolvedValue({ id: 2, name: 'Editor' });
const fd = new FormData();
fd.set('name', 'Editor');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(create).toHaveBeenCalledWith({ data: { name: 'Editor' } });
expect(result).toEqual({ success: true });
});
it('updates an existing record when id is present', async () => {
findUnique.mockResolvedValue({ id: 1, name: 'Old Name' });
const fd = new FormData();
fd.set('id', '1');
fd.set('name', 'New Name');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { name: 'New Name' } });
expect(result).toEqual({ success: true });
});
it('returns error when updating non-existent record', async () => {
findUnique.mockResolvedValue(null);
const fd = new FormData();
fd.set('id', '999');
fd.set('name', 'Ghost');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Record not found' });
});
});
describe('crud deleteRecord', () => {
beforeEach(() => {
deletes.mockReset();
});
it('deletes a record by id', async () => {
deletes.mockResolvedValue({ id: 1, name: 'Admin' });
const fd = new FormData();
fd.set('id', '1');
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(deletes).toHaveBeenCalledWith({ where: { id: 1 } });
expect(result).toEqual({ success: true });
});
it('returns error when id is missing', async () => {
const fd = new FormData();
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Missing id' });
});
});
+202
View File
@@ -0,0 +1,202 @@
import { getDelegate, parseId } from './prisma-delegate';
import { extractFormData } from './form-data';
import type { AdminModelConfig } from './types';
import { superValidate } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import * as Schemas from './schemas';
import type { z } from 'zod';
const modelSchemas: Record<string, z.ZodObject<any>> = {
User: Schemas.User,
Profile: Schemas.Profile,
UserRole: Schemas.UserRole,
Proclamation: Schemas.Proclamation,
ProclamationCategory: Schemas.ProclamationCategory,
SolfeggioFrequency: Schemas.SolfeggioFrequency,
SubscriptionLevel: Schemas.SubscriptionLevel,
LicenseKey: Schemas.LicenseKey
};
export function superFormErrorsToFieldErrors(superErrors: Record<string, unknown>): {
error: string;
fieldErrors: Record<string, string>;
} {
const fieldErrors: Record<string, string> = {};
let formError = '';
for (const [key, value] of Object.entries(superErrors)) {
if (key === '_errors' && Array.isArray(value)) {
formError = (value[0] as string) ?? '';
} else if (
value &&
typeof value === 'object' &&
'_errors' in value &&
Array.isArray((value as Record<string, unknown>)._errors)
) {
const errMsg = ((value as Record<string, unknown>)._errors as string[])[0];
fieldErrors[key] = errMsg;
}
}
return { error: formError, fieldErrors };
}
export async function loadRecords(locals: App.Locals, config: AdminModelConfig) {
const delegate = getDelegate(config);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
const records = await delegate.findMany({
where,
include: config.include,
orderBy: { id: 'asc' }
});
const relationOptionsData: Record<string, { label: string; value: string | number }[]> = {};
// For cascade child fields: maps field -> { childValue -> parentValue }
const cascadeOptionParentMap: Record<string, Record<string | number, string | number>> = {};
for (const [field, relConfig] of Object.entries(config.relationOptions ?? {})) {
const relDelegate = getDelegate({ ...config, modelName: relConfig.model });
const selectFields: Record<string, boolean> = {
[relConfig.valueField]: true,
[relConfig.labelField]: true
};
// Check if this field is a cascade child (needs parentId)
const cascadeEntry = Object.entries(config.cascadeFields ?? {}).find(
([, c]) => c.parentField === field
);
if (cascadeEntry) {
selectFields['parentId'] = true;
}
const options = await relDelegate.findMany({
select: selectFields,
orderBy: { [relConfig.labelField]: 'asc' }
});
const mapped = (options as Record<string, unknown>[]).map((opt: Record<string, unknown>) => ({
label: String(opt[relConfig.labelField]),
value: opt[relConfig.valueField] as string | number
}));
relationOptionsData[field] = mapped;
// Build parent map if cascade child
if (cascadeEntry) {
const childField = cascadeEntry[0];
const parentMap: Record<string | number, string | number> = {};
const rows = options as Record<string, unknown>[];
for (const row of rows) {
const childVal = row[relConfig.valueField] as string | number;
const parentVal = row['parentId'] as number | null;
if (parentVal != null) {
parentMap[childVal] = parentVal;
}
}
cascadeOptionParentMap[childField] = parentMap;
}
}
return {
records: records as Record<string, unknown>[],
relationOptionsData,
cascadeOptionParentMap
};
}
export async function upsertRecord(
requestOrFormData: Request | FormData,
config: AdminModelConfig,
locals: App.Locals
) {
const schema = modelSchemas[config.modelName];
const form = schema ? await superValidate(requestOrFormData, zod4(schema)) : null;
if (form && !form.valid) {
const { error, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { ...form, success: false, error, fieldErrors };
}
const formData =
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
const rawId = formData.get('id')?.toString();
const data =
form?.data ??
(extractFormData(formData, config.formFields, config.fieldTypes) as Record<string, unknown>);
// Strip virtual/UI-only fields from Prisma payload
for (const f of config.dataOnlyFields ?? []) {
delete data[f];
}
const delegate = getDelegate(config);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
if (rawId) {
const id = parseId(config, rawId);
const existing = await delegate.findUnique({
where: { id },
...((config.include ? { include: config.include } : {}) as object)
});
if (!existing) {
return { success: false, error: 'Record not found' };
}
if (where) {
const match = await delegate.findFirst({
where: { id, ...where }
});
if (!match) {
return { success: false, error: 'Access denied' };
}
}
await delegate.update({
where: { id },
data
});
return { success: true };
} else {
try {
await delegate.create({ data });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
const meta = (e as { meta?: { target?: string[] } }).meta;
const fields = meta?.target ?? [];
return {
success: false,
error: `A record with this ${fields.join(', ')} already exists.`,
fieldErrors: Object.fromEntries(fields.map((f) => [f, 'Already taken']))
};
}
throw e;
}
return { success: true };
}
}
export async function deleteRecord(
requestOrFormData: Request | FormData,
config: AdminModelConfig,
locals: App.Locals
) {
const delegate = getDelegate(config);
const formData =
requestOrFormData instanceof FormData ? requestOrFormData : await requestOrFormData.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' };
const id = parseId(config, rawId);
const viewerRoleId = (locals.user as { roleId?: number } | null)?.roleId ?? 4;
const where = config.accessFilter?.(viewerRoleId);
if (where) {
const match = await delegate.findFirst({ where: { id, ...where } });
if (!match) return { success: false, error: 'Access denied' };
}
await delegate.delete({ where: { id } });
return { success: true };
}
+4
View File
@@ -31,6 +31,10 @@ export function extractFormData(
data[field] = val === '' ? null : new Date(val);
break;
}
case 'Tiptap': {
data[field] = raw?.toString() ?? '';
break;
}
default: {
data[field] = raw?.toString() ?? '';
break;
+74
View File
@@ -0,0 +1,74 @@
import { z } from 'zod';
const email = z.string().email('Invalid email address');
const phone = z
.string()
.min(10, 'Phone number must be at least 10 digits')
.regex(/^[\d\-\+\(\) ]+$/, 'Invalid phone number format');
const name = z.string().min(1, 'Required').max(100, 'Too long');
export const UserRole = z.object({
name: z.string().min(1, 'Required')
});
export const User = z.object({
name: z.string().min(1, 'Required'),
email,
roleId: z.coerce.number().int().min(1)
});
export const Profile = z.object({
userId: z.string().min(1, 'Required'),
firstName: name,
lastName: name,
phoneNumber: phone
});
export const Address = z.object({
profileId: z.string().min(1),
street: z.string().min(1, 'Street is required'),
city: z.string().min(1, 'City is required'),
state: z.string().min(1, 'State is required'),
postalCode: z.string().min(1, 'Postal code is required'),
country: z.string().min(1, 'Country is required'),
type: z.enum(['Mailing', 'Billing', 'Shipping']).default('Mailing'),
isDefaultMailing: z.coerce.boolean().default(false),
isDefaultBilling: z.coerce.boolean().default(false),
isDefaultShipping: z.coerce.boolean().default(false)
});
export const SolfeggioFrequency = z.object({
name: z.string().min(1, 'Required'),
value: z.coerce.number().int('Must be a whole number')
});
export const SubscriptionLevel = z.object({
name: z.string().min(1, 'Required'),
description: z.string().optional().default('')
});
export const ProclamationCategory = z.object({
name: z.string().min(1, 'Required'),
parentId: z.coerce.number().int().optional()
});
export const Proclamation = z.object({
name: z.string().min(1, 'Required'),
solfeggioFrequencyId: z.coerce.number().int(),
parentCategoryId: z.coerce.number().int(),
categoryId: z.coerce.number().int(),
subscriptionLevelId: z.coerce.number().int(),
content: z.string().optional().default('')
});
export const LicenseKey = z.object({
active: z.coerce.boolean(),
level: z.string().min(1, 'Required'),
userId: z.string().min(1, 'Required')
});
export const UserUpdate = z.object({
id: z.string().min(1),
name: z.string().min(1, 'Required').optional(),
email: z.string().email().optional()
});
+9
View File
@@ -19,6 +19,12 @@ export type RelatedRecordConfig = {
editSlug?: string;
};
export type ActionResult = {
success: boolean;
error?: string;
fieldErrors?: Record<string, string>;
};
export type AdminModelConfig = {
slug: string;
modelName: string;
@@ -35,4 +41,7 @@ export type AdminModelConfig = {
relationOptions?: Record<string, RelationOptionConfig>;
relatedRecords?: RelatedRecordConfig[];
accessFilter?: (viewerRoleId: number) => Record<string, unknown> | undefined;
/** Fields that should be excluded from Prisma create/update (virtual/UI-only) */
dataOnlyFields?: string[];
cascadeFields?: Record<string, { parentField: string }>;
};
@@ -37,12 +37,16 @@
<h2 class="mb-4 text-lg font-medium text-gray-900 dark:text-gray-100">Profile</h2>
{#if form?.action === 'updateProfile' && form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Profile updated successfully.
</div>
{/if}
{#if form?.action === 'updateProfile' && form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -115,12 +119,16 @@
</div>
{#if form?.action === 'addAddress' && form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
Address added successfully.
</div>
{/if}
{#if form?.action === 'addAddress' && form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -133,7 +141,9 @@
>
<div class="flex flex-col gap-3">
<div>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Street</label>
<label for="street" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Street</label
>
<input
type="text"
id="street"
@@ -144,7 +154,9 @@
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300">City</label>
<label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>City</label
>
<input
type="text"
id="city"
@@ -154,7 +166,9 @@
/>
</div>
<div>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300">State</label>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>State</label
>
<input
type="text"
id="state"
@@ -166,7 +180,10 @@
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="postalCode" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label
for="postalCode"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Postal Code
</label>
<input
@@ -178,7 +195,10 @@
/>
</div>
<div>
<label for="country" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
<label
for="country"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label
>
<input
type="text"
id="country"
@@ -204,10 +224,13 @@
{#if data.user.profile?.addresses && data.user.profile.addresses.length > 0}
<div class="flex flex-col gap-3">
{#each data.user.profile.addresses as address}
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
<div
class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm text-gray-900 dark:text-gray-100">{address.street}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
{address.city}, {address.state} {address.postalCode}
{address.city}, {address.state}
{address.postalCode}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{address.country}</p>
</div>
@@ -168,9 +168,9 @@ describe('(protected)/account page server', () => {
country: 'US'
});
const err = await Promise.resolve(
invokeAction('addAddress', request, createLocals())
).catch((e: unknown) => e);
const err = await Promise.resolve(invokeAction('addAddress', request, createLocals())).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
@@ -201,9 +201,9 @@ describe('(protected)/account page server', () => {
});
const locals = createLocals(mockUser() as any);
const err = await Promise.resolve(
invokeAction('addAddress', request, locals)
).catch((e: unknown) => e);
const err = await Promise.resolve(invokeAction('addAddress', request, locals)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 400)).toBe(true);
});
@@ -1,4 +1,3 @@
<!--
There should be a form in the middle
rounded corners
@@ -44,17 +43,29 @@
<Window>
<form method="POST" class="border p-6 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100">
<p class="mx-auto text-center my-4 italic">Please finish setting up your profile</p>
<p class="mx-auto my-4 text-center italic">Please finish setting up your profile</p>
{#if data.user?.image}
<img src={data.user?.image} class="rounded-full w-[8rem] mx-auto" alt="Profile" />
<img src={data.user?.image} class="mx-auto w-[8rem] rounded-full" alt="Profile" />
{:else}
<div class="mx-auto aspect-square w-[8rem] rounded-full bg-slate-900"></div>
{/if}
<p class="mx-auto text-center my-4">{data.user?.email}</p>
<p class="mx-auto my-4 text-center">{data.user?.email}</p>
<div class="flex flex-col">
<input type="text" name="userId" value={data.user?.id} class="hidden"/>
<input type="text" name="firstName" placeholder="First Name" class="my-2" bind:value={firstName} />
<input type="text" name="lastName" placeholder="Last Name" class="my-2" bind:value={lastName} />
<input type="text" name="userId" value={data.user?.id} class="hidden" />
<input
type="text"
name="firstName"
placeholder="First Name"
class="my-2"
bind:value={firstName}
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
class="my-2"
bind:value={lastName}
/>
<input
type="tel"
name="phoneNumber"
@@ -64,6 +75,8 @@
oninput={handlePhoneInput}
/>
</div>
<button type="submit" class="p-2 px-4 bg-blue-300 dark:bg-gray-700 dark:text-gray-100">Submit</button>
<button type="submit" class="bg-blue-300 p-2 px-4 dark:bg-gray-700 dark:text-gray-100"
>Submit</button
>
</form>
</Window>
@@ -33,9 +33,9 @@ describe('account setup page server load', () => {
});
it('throws 403 when user is not logged in', async () => {
const err = await Promise.resolve(
load({ locals: createLocals() } as any)
).catch((e: unknown) => e);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
@@ -5,7 +5,9 @@
</script>
<div class="flex h-full min-h-0 flex-col gap-6 p-6 dark:text-gray-100">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Welcome, {data.firstName}!</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
Welcome, {data.firstName}!
</h1>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<a
@@ -22,7 +24,9 @@
class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Proclamations</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{data.proclamationCount}</p>
<p class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">
{data.proclamationCount}
</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">Browse all</p>
</a>
@@ -12,13 +12,19 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Key
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Level
</th>
<th class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase dark:text-gray-400"
>
Status
</th>
</tr>
@@ -26,15 +32,17 @@
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-900">
{#each data.licenseKeys as key}
<tr>
<td class="whitespace-nowrap px-6 py-4 font-mono text-sm text-gray-900 dark:text-gray-100">
<td
class="px-6 py-4 font-mono text-sm whitespace-nowrap text-gray-900 dark:text-gray-100"
>
{key.key}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-700 dark:text-gray-300">
{key.level}
</td>
<td class="whitespace-nowrap px-6 py-4 text-sm">
<td class="px-6 py-4 text-sm whitespace-nowrap">
<span
class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 {key.active
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold {key.active
? 'bg-green-100 text-green-800 dark:bg-gray-700 dark:text-gray-200'
: 'bg-red-100 text-red-800 dark:bg-gray-700 dark:text-gray-200'}"
>
@@ -23,7 +23,7 @@ export const load: PageServerLoad = async ({ url }) => {
const [proclamations, categories] = await Promise.all([
prisma.proclamation.findMany({
where,
include: { solfeggioFrequency: true, category: { include: { parent: true } } },
include: { solfeggioFrequency: true, category: { include: { parent: true } }, subscriptionLevel: true },
orderBy: { name: 'asc' }
}),
prisma.proclamationCategory.findMany({ include: { parent: true }, orderBy: { name: 'asc' } })
@@ -40,7 +40,9 @@
<!-- Filters -->
<div class="flex flex-wrap items-end gap-4">
<div>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Search</label>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Search</label
>
<input
type="text"
id="search"
@@ -51,7 +53,9 @@
/>
</div>
<div>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Category</label>
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>Category</label
>
<select
id="category"
bind:value={categoryId}
@@ -60,7 +64,7 @@
>
<option value={null}>All Categories</option>
{#each data.categories as cat}
<option value={cat.id}>{displayCategoryName(cat)}</option>
<option value={cat.id}>{displayCategoryName(cat)}</option>
{/each}
</select>
</div>
@@ -91,14 +95,16 @@
class="flex flex-col rounded-lg border border-gray-200 bg-white p-5 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-900"
>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">{proclamation.name}</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{displayCategoryName(proclamation.category)}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{displayCategoryName(proclamation.category)}
</p>
<div class="mt-auto flex items-center justify-between pt-4">
<span class="text-sm text-purple-600 dark:text-gray-400">
{proclamation.solfeggioFrequency.value} Hz &mdash;
{proclamation.solfeggioFrequency.name}
</span>
<span class="font-semibold text-gray-900 dark:text-gray-100">
${proclamation.price.toFixed(2)}
<span class="text-xs text-gray-500 dark:text-gray-400">
{proclamation.subscriptionLevel?.name ?? 'Standard'}
</span>
</div>
</a>
@@ -14,7 +14,8 @@ export const load: PageServerLoad = async ({ params, parent }) => {
where: { id: params.id },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } }
category: { include: { parent: true } },
subscriptionLevel: true
}
});
@@ -19,27 +19,35 @@
</a>
<div>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">{data.proclamation.name}</h1>
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{data.proclamation.name}
</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{categoryBreadcrumb}</p>
</div>
<div class="flex flex-wrap gap-4">
<div class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800">
<div
class="rounded-lg border border-purple-200 bg-purple-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-purple-600 dark:text-gray-400">Frequency</p>
<p class="text-sm font-semibold text-purple-900 dark:text-gray-100">
{data.proclamation.solfeggioFrequency.value} Hz &mdash;
{data.proclamation.solfeggioFrequency.name}
</p>
</div>
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Price</p>
<div
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800"
>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Level</p>
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
${data.proclamation.price.toFixed(2)}
{data.proclamation.subscriptionLevel?.name ?? 'Standard'}
</p>
</div>
</div>
<div class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert">
<div
class="prose max-w-none rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900 dark:prose-invert"
>
{@html data.proclamation.content}
</div>
</div>
@@ -21,9 +21,9 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
it('throws 404 when proclamation is not found', async () => {
findUnique.mockResolvedValue(null);
const err = await Promise.resolve(
load({ params: { id: 'nonexistent' } } as any)
).catch((e: unknown) => e);
const err = await Promise.resolve(load({ params: { id: 'nonexistent' } } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 404)).toBe(true);
});
@@ -44,7 +44,8 @@ describe('(protected)/admin/proclamations/[id] page server load', () => {
where: { id: 'proc-1' },
include: {
solfeggioFrequency: true,
category: { include: { parent: true } }
category: { include: { parent: true } },
subscriptionLevel: true
}
});
});
@@ -14,7 +14,7 @@ import { load } from './+page.server';
const proclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
describe('(protected)/admin/proclamations page server load', () => {
describe('(protected)/admin/proclamations page server load', () => {
beforeEach(() => {
proclamationFindMany.mockReset();
categoryFindMany.mockReset();
@@ -29,10 +29,12 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
const url = createUrl('/admin/proclamations');
const result = (await load({ url } as any)) as any;
expect(result.proclamations).toEqual(proclamations);
expect(result.categories[0]).toEqual(expect.objectContaining({
...categories[0],
displayName: categories[0].name
}));
expect(result.categories[0]).toEqual(
expect.objectContaining({
...categories[0],
displayName: categories[0].name
})
);
expect(result.search).toBe('');
expect(result.categoryId).toBeNull();
});
@@ -109,7 +111,7 @@ const categoryFindMany = prisma.proclamationCategory.findMany as ReturnType<type
expect(proclamationFindMany).toHaveBeenCalledWith(
expect.objectContaining({
include: { solfeggioFrequency: true, category: { include: { parent: true } } }
include: { solfeggioFrequency: true, category: { include: { parent: true } }, subscriptionLevel: true }
})
);
});
+1 -3
View File
@@ -48,9 +48,7 @@
{#each adminModels as model}
<a
href="/admin/{model.slug}"
class="rounded px-3 py-1 text-sm transition-colors {isActive(
'/admin/' + model.slug
)
class="rounded px-3 py-1 text-sm transition-colors {isActive('/admin/' + model.slug)
? 'bg-blue-50 text-blue-700 dark:bg-gray-800 dark:text-gray-100'
: 'text-blue-600 hover:bg-gray-50 hover:text-blue-800 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100'}"
>
@@ -3,10 +3,10 @@ import { error } from '@sveltejs/kit';
import { prisma } from '$lib/db';
import { canAccessAdmin } from '$lib/svelte-admin/roles';
/**
/**
* This Layout Server-Side page checks if the user is logged in
* and if that user has a sufficient permission level to view any pages
* rendering this layout, redirecting them to a 403 error page if not.
* rendering this layout, redirecting them to a 403 error page if not.
* It also makes the `adminUser` (the currently logged in user) available
* to all of those pages via page data.
*/
@@ -18,7 +18,9 @@ export const load: LayoutServerLoad = async ({ locals }) => {
});
if (!user || !canAccessAdmin(user.roleId)) {
console.log(`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`);
console.log(
`Attempted to access /admin via ${user?.id} : ${user?.name} with a role of ${user?.role}`
);
throw error(403, 'not authorized');
}
@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';
import { getAllModelConfigs } from '$lib/svelte-admin/registry';
export const load: PageServerLoad = async () => {
const models = getAllModelConfigs().filter((m) => m.slug !== 'license-keys');
return { models };
};
+21 -1
View File
@@ -1 +1,21 @@
<h1 class="p-6 dark:text-gray-100">Admin Dashboard</h1>
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let models = $derived(data.models);
</script>
<h1 class="p-6 text-xl font-bold text-gray-900 dark:text-gray-100">Admin Dashboard</h1>
<div class="grid grid-cols-1 gap-4 px-6 pb-6 sm:grid-cols-2 lg:grid-cols-3">
{#each models as model (model.slug)}
<a
href={'/admin/' + model.slug}
class="block border border-gray-200 p-4 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
>
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{model.displayName}</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{model.listFields.length} fields
</p>
</a>
{/each}
</div>
@@ -1,10 +0,0 @@
import type { PageServerLoad } from "./$types";
import { prisma } from "$lib/db";
export const load: PageServerLoad = async ({ locals }) => {
console.log(locals.user?.name);
// Load proclamation categories
const categories = await prisma.proclamationCategory.findMany();
console.log(categories);
return { categories };
}
@@ -1,41 +0,0 @@
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
function getCategoryNameFromId(id: number): string {
if (data.categories) {
const target = data.categories.find((category) => category.id === id);
return target ? target.name : '';
}
return '';
}
</script>
<!-- Display a list of categories -->
<div class="p-6 dark:text-gray-100">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="hidden">ID</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500 dark:text-gray-400">Name</th>
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500 dark:text-gray-400">Parent Category</th>
</tr>
</thead>
<tbody>
{#if data.categories}
{#each data.categories as category, i (category.id)}
<tr class="border-b border-gray-200 hover:bg-gray-300 dark:border-gray-700 dark:hover:bg-gray-800">
<td class="hidden">{i}</td>
<td class="px-4 py-2 text-gray-900 dark:text-gray-100">{category.name}</td>
{#if category.parentId}
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">{getCategoryNameFromId(category.parentId)}</td>
{:else}
<td class="px-4 py-2"></td>
{/if}
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
@@ -0,0 +1,68 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords } from '$lib/svelte-admin/crud';
import { prisma } from '$lib/db';
import { sendMail } from '$lib/server/resend';
import { error } from '@sveltejs/kit';
const config = getModelConfig('license-keys')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
createKey: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const email = formData.get('email')?.toString();
const deactivateOld = formData.get('deactivateOld') === 'on';
if (!email) {
return { success: false, error: 'Email is required' };
}
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return { success: false, error: 'No user found with that email' };
}
// Deactivate old keys if requested
if (deactivateOld) {
await prisma.licenseKey.updateMany({
where: { userId: user.id, active: true },
data: { active: false }
});
}
// Generate 16-char hex UUID grouped as 4-4-4-4
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
const key = `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`;
const licenseKey = await prisma.licenseKey.create({
data: {
key,
active: true,
level: 'standard',
userId: user.id
}
});
// Send email with license key
try {
await sendMail({
to: email,
subject: 'Your License Key',
data: {
firstName: user.name ?? 'Valued Customer',
licenseKey: key
}
});
} catch (e) {
console.error('Failed to send license key email:', e);
}
return { success: true, error: undefined };
}
};
@@ -0,0 +1,100 @@
<script lang="ts">
import type { PageProps } from './$types';
import type { ActionResult } from '$lib/svelte-admin/types';
import AdminTable from '$lib/components/admin/AdminTable.svelte';
import Toast from '$lib/components/admin/Toast.svelte';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { enhance } from '$app/forms';
let { data, form }: PageProps = $props();
const config = getModelConfig('license-keys')!;
let showCreateForm = $state(false);
let toastMessage = $state('');
let toastType = $state<'success' | 'error'>('success');
let createError = $state('');
$effect(() => {
if (form && 'success' in (form as ActionResult)) {
const result = form as ActionResult;
if (result.success) {
toastMessage = 'License key created and emailed';
toastType = 'success';
showCreateForm = false;
} else {
createError = result.error ?? 'Failed to create license key';
}
}
});
function toggleForm() {
showCreateForm = !showCreateForm;
createError = '';
}
</script>
<div class="p-6">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">{config.displayName}</h1>
<button
type="button"
onclick={toggleForm}
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300"
>
{showCreateForm ? 'Cancel' : 'Create Key +'}
</button>
</div>
{#if showCreateForm}
<form method="POST" action="?/createKey" use:enhance class="mb-6 border border-gray-200 p-4 dark:border-gray-700">
<h2 class="mb-3 text-sm font-semibold text-gray-700 dark:text-gray-300">
Create New License Key
</h2>
{#if createError}
<div class="mb-3 border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
{createError}
</div>
{/if}
<div class="flex flex-col gap-3">
<div>
<label for="email" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
User Email
</label>
<input
type="email"
id="email"
name="email"
required
class="w-full border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
placeholder="user@example.com"
/>
</div>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input type="checkbox" name="deactivateOld" class="h-4 w-4" />
Deactivate existing active keys for this user
</label>
<div class="flex justify-end">
<button
type="submit"
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Create & Email Key
</button>
</div>
</div>
</form>
{/if}
<AdminTable
records={data.records}
listFields={config.listFields}
searchFields={config.searchFields}
fieldTypes={config.fieldTypes}
singular={config.displayNameSingular}
/>
</div>
{#if toastMessage}
<Toast message={toastMessage} type={toastType} />
{/if}
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('proclamation-categories')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="proclamation-categories" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('proclamations')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="proclamations" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -1,8 +1,16 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
import {
loadRecords,
upsertRecord,
deleteRecord,
superFormErrorsToFieldErrors
} from '$lib/svelte-admin/crud';
import { prisma } from '$lib/db';
import { error } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms';
import { zod4 } from 'sveltekit-superforms/adapters';
import * as Schemas from '$lib/svelte-admin/schemas';
const config = getModelConfig('profiles')!;
@@ -12,67 +20,73 @@ export const load: PageServerLoad = async ({ locals }) => {
export const actions = {
upsert: async ({ request, locals }) => {
const formData = await request.formData();
return upsertRecord(formData, config, locals);
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
const formData = await request.formData();
return deleteRecord(formData, config, locals);
return deleteRecord(request, config, locals);
},
upsertAddress: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const id = formData.get('id')?.toString();
const profileId = formData.get('profileId')?.toString();
const form = await superValidate(request, zod4(Schemas.Address));
if (!form.valid) {
const { error: addrError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, addressError: addrError, addressFieldErrors: fieldErrors };
}
const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } =
form.data;
if (!profileId) {
return { success: false, addressError: 'Missing profile ID' };
}
const type = formData.get('type')?.toString() || 'Mailing';
const street = formData.get('street')?.toString() || '';
const city = formData.get('city')?.toString() || '';
const state = formData.get('state')?.toString() || '';
const postalCode = formData.get('postalCode')?.toString() || '';
const country = formData.get('country')?.toString() || '';
const isDefault = formData.get('isDefault') === 'on';
if (!street || !city || !state || !postalCode || !country) {
return {
success: false,
addressError: 'All address fields are required'
};
}
if (isDefault) {
// Unique-default enforcement: clear defaults of same type before setting
if (isDefaultMailing) {
await prisma.address.updateMany({
where: { profileId, type, isDefault: true },
data: { isDefault: false }
where: { profileId, isDefaultMailing: true },
data: { isDefaultMailing: false }
});
}
if (isDefaultBilling) {
await prisma.address.updateMany({
where: { profileId, isDefaultBilling: true },
data: { isDefaultBilling: false }
});
}
if (isDefaultShipping) {
await prisma.address.updateMany({
where: { profileId, isDefaultShipping: true },
data: { isDefaultShipping: false }
});
}
if (id) {
const updated = await prisma.address.update({
where: { id },
data: { type, street, city, state, postalCode, country, isDefault }
});
return { success: true, address: updated };
}
const payload = {
...addressData,
profileId,
isDefaultMailing,
isDefaultBilling,
isDefaultShipping
};
const created = await prisma.address.create({
data: { profileId, type, street, city, state, postalCode, country, isDefault }
});
const created = await prisma.address.create({ data: payload });
return { success: true, address: created };
},
updateUser: async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const formData = await request.formData();
const userId = formData.get('id')?.toString();
const email = formData.get('email')?.toString();
const name = formData.get('name')?.toString();
const form = await superValidate(request, zod4(Schemas.UserUpdate));
if (!form.valid) {
const { error: userErr, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, userError: userErr, userFieldErrors: fieldErrors };
}
if (!userId) {
const { id, email, name } = form.data;
if (!id) {
return { success: false, userError: 'Missing user ID' };
}
@@ -83,7 +97,7 @@ export const actions = {
return { success: false, userError: 'No fields to update' };
}
await prisma.user.update({ where: { id: userId }, data });
await prisma.user.update({ where: { id }, data });
return { success: true };
}
};
@@ -30,8 +30,9 @@
state: '',
postalCode: '',
country: '',
type: 'Mailing',
isDefault: 'false'
isDefaultMailing: 'false',
isDefaultBilling: 'false',
isDefaultShipping: 'false'
});
let userEmail = $state('');
@@ -44,11 +45,9 @@
} else if ((form as any).userError) {
userError = (form as any).userError;
} else if (form.success) {
toastMessage = (form as any).addressError
? 'Address saved'
: 'Saved successfully';
toastType = 'success';
if ((form as any).address) {
toastMessage = 'Address saved';
const addr = (form as any).address as Record<string, unknown>;
if (editingRecord) {
const addrs = (editingRecord.addresses as Record<string, unknown>[]) ?? [];
@@ -59,7 +58,8 @@
}
addressFormOpen = false;
addressError = '';
} else if (!(form as any).addressError && !(form as any).userError) {
} else {
toastMessage = 'Saved successfully';
closeModal();
}
} else if (!(form as any).addressError && !(form as any).userError) {
@@ -86,8 +86,8 @@
userFormOpen = false;
addressError = '';
userError = '';
userEmail = (record.user as Record<string, unknown>)?.email as string ?? '';
userName = (record.user as Record<string, unknown>)?.name as string ?? '';
userEmail = ((record.user as Record<string, unknown>)?.email as string) ?? '';
userName = ((record.user as Record<string, unknown>)?.name as string) ?? '';
modalOpen = true;
}
@@ -162,11 +162,16 @@
</button>
</div>
<p class="mb-1 text-sm text-gray-600 dark:text-gray-400">
{user?.name as string ?? ''} ({user?.email as string ?? ''})
{(user?.name as string) ?? ''} ({(user?.email as string) ?? ''})
</p>
{#if userFormOpen}
<form method="POST" action="?/updateUser" use:enhance class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700">
<form
method="POST"
action="?/updateUser"
use:enhance
class="mt-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<input type="hidden" name="id" value={String(user?.id ?? '')} />
{#if userError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
@@ -175,7 +180,10 @@
{/if}
<div class="flex flex-col gap-2">
<div>
<label for="user-name" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="user-name"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Name
</label>
<input
@@ -187,7 +195,10 @@
/>
</div>
<div>
<label for="user-email" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="user-email"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Email
</label>
<input
@@ -227,7 +238,12 @@
</div>
{#if addressFormOpen}
<form method="POST" action="?/upsertAddress" use:enhance class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700">
<form
method="POST"
action="?/upsertAddress"
use:enhance
class="mb-3 rounded border border-gray-200 p-3 dark:border-gray-700"
>
<input type="hidden" name="profileId" value={String(editingRecord.id)} />
{#if addressError}
<div class="mb-2 border border-red-300 bg-red-50 px-3 py-1.5 text-sm text-red-700">
@@ -236,21 +252,10 @@
{/if}
<div class="grid grid-cols-2 gap-2">
<div>
<label for="addr-type" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
Type
</label>
<select
id="addr-type"
name="type"
class="w-full border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
<label
for="addr-street"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
<option value="Mailing">Mailing</option>
<option value="Billing">Billing</option>
<option value="Shipping">Shipping</option>
</select>
</div>
<div>
<label for="addr-street" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
Street
</label>
<input
@@ -262,7 +267,10 @@
/>
</div>
<div>
<label for="addr-city" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-city"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
City
</label>
<input
@@ -274,7 +282,10 @@
/>
</div>
<div>
<label for="addr-state" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-state"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
State
</label>
<input
@@ -286,7 +297,10 @@
/>
</div>
<div>
<label for="addr-postal" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-postal"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Postal Code
</label>
<input
@@ -298,7 +312,10 @@
/>
</div>
<div>
<label for="addr-country" class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400">
<label
for="addr-country"
class="mb-0.5 block text-xs font-medium text-gray-600 dark:text-gray-400"
>
Country
</label>
<input
@@ -310,10 +327,20 @@
/>
</div>
</div>
<label class="mt-2 flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefault" class="h-3.5 w-3.5" />
Set as default for this type
</label>
<div class="mt-2 flex flex-wrap gap-4">
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultMailing" class="h-3.5 w-3.5" />
Default Mailing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultBilling" class="h-3.5 w-3.5" />
Default Billing
</label>
<label class="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400">
<input type="checkbox" name="isDefaultShipping" class="h-3.5 w-3.5" />
Default Shipping
</label>
</div>
<div class="mt-2 flex justify-end">
<button
type="submit"
@@ -329,25 +356,30 @@
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Street</th
>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">City</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">State</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Zip</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Country</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase">Default</th>
<th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase"
>Country</th
>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">M</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">B</th>
<th class="px-2 py-1 text-center text-xs font-medium text-gray-500 uppercase">S</th>
</tr>
</thead>
<tbody>
{#each addresses as addr}
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="px-2 py-1 text-xs">{addr.type as string}</td>
<td class="px-2 py-1">{addr.street as string}</td>
<td class="px-2 py-1">{addr.city as string}</td>
<td class="px-2 py-1">{addr.state as string}</td>
<td class="px-2 py-1">{addr.postalCode as string}</td>
<td class="px-2 py-1">{addr.country as string}</td>
<td class="px-2 py-1">{addr.isDefault ? 'Yes' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultMailing ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultBilling ? '✓' : ''}</td>
<td class="px-2 py-1 text-center text-xs">{addr.isDefaultShipping ? '✓' : ''}</td>
</tr>
{/each}
</tbody>
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('solfeggio-frequencies')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="solfeggio-frequencies" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('subscription-levels')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="subscription-levels" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('user-roles')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="user-roles" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
@@ -0,0 +1,18 @@
import type { PageServerLoad, Actions } from './$types';
import { getModelConfig } from '$lib/svelte-admin/registry';
import { loadRecords, upsertRecord, deleteRecord } from '$lib/svelte-admin/crud';
const config = getModelConfig('users')!;
export const load: PageServerLoad = async ({ locals }) => {
return loadRecords(locals, config);
};
export const actions: Actions = {
upsert: async ({ request, locals }) => {
return upsertRecord(request, config, locals);
},
delete: async ({ request, locals }) => {
return deleteRecord(request, config, locals);
}
};
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageProps } from './$types';
import AdminPage from '$lib/components/admin/AdminPage.svelte';
let { data, form }: PageProps = $props();
</script>
<AdminPage modelSlug="users" {data} {form} adminRoleId={data.adminUser?.roleId ?? 4} />
+3 -1
View File
@@ -25,7 +25,9 @@ describe('(protected) layout server load', () => {
it('throws 403 when locals.user is undefined', async () => {
findUnique.mockResolvedValue(null);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch((e: unknown) => e);
const err = await Promise.resolve(load({ locals: createLocals() } as any)).catch(
(e: unknown) => e
);
expect(isHttpError(err, 403)).toBe(true);
});
+6 -3
View File
@@ -6,8 +6,11 @@
<Window>
{#if $page.status === 403}
<h1 class="dark:text-gray-100">Not Authorized</h1>
<p class="dark:text-gray-300">
You don't have permission to view this page. <a href="/" class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a>.
</p>
<p class="dark:text-gray-300">
You don't have permission to view this page. <a
href="/"
class="dark:text-gray-400 dark:hover:text-gray-200">Go home</a
>.
</p>
{/if}
</Window>
@@ -1,21 +1,21 @@
import type { RequestHandler } from "./$types";
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async () => {
// Request validation (400)
// 1. Validate and extract information
// Request validation (400)
// 1. Validate and extract information
// Business rule validation (409)
// 2. Check for existing entries
// Business rule validation (409)
// 2. Check for existing entries
// Resource creation (500 on unexpected failures)
// 3. Create new entries
// 5. Create license key
// Resource creation (500 on unexpected failures)
// 3. Create new entries
// 5. Create license key
// External operations (502/503/504 or 500, depending on your API)
// 4. Send profile setup email
// 6. Send license key email
// External operations (502/503/504 or 500, depending on your API)
// 4. Send profile setup email
// 6. Send license key email
// Success (200/201)
// 7. Send response
return new Response(JSON.stringify({ "msg": "success" }))
}
// Success (200/201)
// 7. Send response
return new Response(JSON.stringify({ msg: 'success' }));
};
+3 -3
View File
@@ -2,6 +2,6 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => {
const categories = await prisma.proclamationCategory.findMany()
return new Response(JSON.stringify(categories))
}
const categories = await prisma.proclamationCategory.findMany();
return new Response(JSON.stringify(categories));
};
+3 -3
View File
@@ -2,6 +2,6 @@ import type { RequestHandler } from './$types';
import { prisma } from '$lib/db';
export const GET: RequestHandler = async () => {
const frequencies = await prisma.solfeggioFrequency.findMany()
return new Response(JSON.stringify(frequencies))
}
const frequencies = await prisma.solfeggioFrequency.findMany();
return new Response(JSON.stringify(frequencies));
};
+10 -3
View File
@@ -9,13 +9,17 @@
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Forgot Password</h1>
{#if form?.success}
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-green-200 bg-green-50 p-4 text-green-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
If an account with that email exists, we've sent a password reset link.
</div>
{/if}
{#if form?.error && !form?.success}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -36,7 +40,10 @@
</button>
</form>
<a href={resolve('/login')} class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200">
<a
href={resolve('/login')}
class="mt-4 block text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>
Back to Login
</a>
</div>
+65 -65
View File
@@ -10,68 +10,68 @@
/* Use var(--color-gray-N) so dark-mode inversion flows through */
/* ------------------------------------------------------------------ */
@theme {
--color-blue-50: var(--color-gray-100);
--color-blue-100: var(--color-gray-200);
--color-blue-200: var(--color-gray-300);
--color-blue-300: var(--color-gray-400);
--color-blue-400: var(--color-gray-500);
--color-blue-500: var(--color-gray-500);
--color-blue-600: var(--color-gray-600);
--color-blue-700: var(--color-gray-700);
--color-blue-800: var(--color-gray-800);
--color-green-50: var(--color-gray-100);
--color-green-100: var(--color-gray-200);
--color-green-200: var(--color-gray-300);
--color-green-300: var(--color-gray-400);
--color-green-400: var(--color-gray-500);
--color-green-500: var(--color-gray-500);
--color-green-600: var(--color-gray-600);
--color-green-700: var(--color-gray-700);
--color-green-800: var(--color-gray-800);
--color-red-50: var(--color-gray-100);
--color-red-100: var(--color-gray-200);
--color-red-200: var(--color-gray-300);
--color-red-300: var(--color-gray-400);
--color-red-400: var(--color-gray-500);
--color-red-500: var(--color-gray-500);
--color-red-600: var(--color-gray-600);
--color-red-700: var(--color-gray-700);
--color-red-800: var(--color-gray-800);
--color-purple-50: var(--color-gray-100);
--color-purple-100: var(--color-gray-200);
--color-purple-200: var(--color-gray-300);
--color-purple-300: var(--color-gray-400);
--color-purple-400: var(--color-gray-500);
--color-purple-500: var(--color-gray-500);
--color-purple-600: var(--color-gray-600);
--color-purple-700: var(--color-gray-700);
--color-purple-800: var(--color-gray-800);
--color-purple-900: var(--color-gray-900);
--color-slate-50: var(--color-gray-50);
--color-slate-100: var(--color-gray-100);
--color-slate-200: var(--color-gray-200);
--color-slate-300: var(--color-gray-300);
--color-slate-400: var(--color-gray-400);
--color-slate-500: var(--color-gray-500);
--color-slate-600: var(--color-gray-600);
--color-slate-700: var(--color-gray-700);
--color-slate-800: var(--color-gray-800);
--color-slate-900: var(--color-gray-900);
--color-blue-50: var(--color-gray-100);
--color-blue-100: var(--color-gray-200);
--color-blue-200: var(--color-gray-300);
--color-blue-300: var(--color-gray-400);
--color-blue-400: var(--color-gray-500);
--color-blue-500: var(--color-gray-500);
--color-blue-600: var(--color-gray-600);
--color-blue-700: var(--color-gray-700);
--color-blue-800: var(--color-gray-800);
--color-green-50: var(--color-gray-100);
--color-green-100: var(--color-gray-200);
--color-green-200: var(--color-gray-300);
--color-green-300: var(--color-gray-400);
--color-green-400: var(--color-gray-500);
--color-green-500: var(--color-gray-500);
--color-green-600: var(--color-gray-600);
--color-green-700: var(--color-gray-700);
--color-green-800: var(--color-gray-800);
--color-red-50: var(--color-gray-100);
--color-red-100: var(--color-gray-200);
--color-red-200: var(--color-gray-300);
--color-red-300: var(--color-gray-400);
--color-red-400: var(--color-gray-500);
--color-red-500: var(--color-gray-500);
--color-red-600: var(--color-gray-600);
--color-red-700: var(--color-gray-700);
--color-red-800: var(--color-gray-800);
--color-purple-50: var(--color-gray-100);
--color-purple-100: var(--color-gray-200);
--color-purple-200: var(--color-gray-300);
--color-purple-300: var(--color-gray-400);
--color-purple-400: var(--color-gray-500);
--color-purple-500: var(--color-gray-500);
--color-purple-600: var(--color-gray-600);
--color-purple-700: var(--color-gray-700);
--color-purple-800: var(--color-gray-800);
--color-purple-900: var(--color-gray-900);
--color-slate-50: var(--color-gray-50);
--color-slate-100: var(--color-gray-100);
--color-slate-200: var(--color-gray-200);
--color-slate-300: var(--color-gray-300);
--color-slate-400: var(--color-gray-400);
--color-slate-500: var(--color-gray-500);
--color-slate-600: var(--color-gray-600);
--color-slate-700: var(--color-gray-700);
--color-slate-800: var(--color-gray-800);
--color-slate-900: var(--color-gray-900);
--radius: 0px;
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-3xl: 0px;
--radius: 0px;
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-3xl: 0px;
--shadow-sm: 0 0 #0000;
--shadow: 0 0 #0000;
--shadow-md: 0 0 #0000;
--shadow-lg: 0 0 #0000;
--shadow-xl: 0 0 #0000;
--shadow-2xl: 0 0 #0000;
--shadow-sm: 0 0 #0000;
--shadow: 0 0 #0000;
--shadow-md: 0 0 #0000;
--shadow-lg: 0 0 #0000;
--shadow-xl: 0 0 #0000;
--shadow-2xl: 0 0 #0000;
}
/* ------------------------------------------------------------------ */
@@ -80,9 +80,9 @@
.dark input,
.dark select,
.dark textarea {
background-color: #1f2937;
color: #f9fafb;
border-color: #374151;
background-color: #1f2937;
color: #f9fafb;
border-color: #374151;
}
/* ------------------------------------------------------------------ */
@@ -91,6 +91,6 @@
*,
::before,
::after {
transition: none !important;
animation: none !important;
transition: none !important;
animation: none !important;
}
+11 -7
View File
@@ -37,20 +37,24 @@
{#if error}
<span class="text-red-600 dark:text-gray-300">{error}</span>
{/if}
<form
onsubmit={handleSubmit}
class="my-6 flex flex-col items-center justify-center"
>
<form onsubmit={handleSubmit} class="my-6 flex flex-col items-center justify-center">
<p>Login</p>
<input type="email" placeholder="Email Address" bind:value={email} required class="my-2" />
<input type="password" placeholder="Password" bind:value={password} required class="my-2" />
<button class="mt-4 rounded bg-blue-200 px-4 py-2 dark:bg-gray-700 dark:text-gray-100" type="submit">Sign In</button>
<button
class="mt-4 rounded bg-blue-200 px-4 py-2 dark:bg-gray-700 dark:text-gray-100"
type="submit">Sign In</button
>
</form>
<div class="mt-2 flex flex-col items-center gap-2">
<a href="/forgot-password" class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
<a
href="/forgot-password"
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>Forgot Password?</a
>
<a href="/register" class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
<a
href="/register"
class="text-sm text-blue-600 hover:text-blue-800 dark:text-gray-400 dark:hover:text-gray-200"
>Don't have an account? Register</a
>
</div>
+13 -3
View File
@@ -23,7 +23,9 @@
{:else}
<div class="flex flex-col items-start justify-center">
{#if form?.error}
<div class="mb-4 w-full rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 w-full rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
@@ -32,8 +34,16 @@
<input type="text" name="lastName" placeholder="Last Name" required class="mb-2" />
<input type="email" name="email" placeholder="Email Address" required class="mb-2" />
<input type="password" name="password" placeholder="Password" required class="mb-2" />
<input type="password" name="password2" placeholder="Verify Password" required class="mb-2" />
<button type="submit" class="mx-auto rounded-full bg-blue-600 px-6 py-2 text-white dark:bg-gray-700 dark:text-gray-100"
<input
type="password"
name="password2"
placeholder="Verify Password"
required
class="mb-2"
/>
<button
type="submit"
class="mx-auto rounded-full bg-blue-600 px-6 py-2 text-white dark:bg-gray-700 dark:text-gray-100"
>Submit</button
>
</form>
+3 -1
View File
@@ -8,7 +8,9 @@
<h1 class="mb-6 text-2xl font-semibold text-gray-900 dark:text-gray-100">Reset Password</h1>
{#if form?.error}
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div
class="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{form.error}
</div>
{/if}
+3 -10
View File
@@ -89,8 +89,7 @@ describe('reset-password page server action', () => {
resetPassword.mockReset();
});
const invokeDefault = (url: URL, request: Request) =>
actions.default({ request, url } as any);
const invokeDefault = (url: URL, request: Request) => actions.default({ request, url } as any);
it('returns fail(400) when token is missing', async () => {
const url = createUrl('/reset-password');
@@ -120,10 +119,7 @@ describe('reset-password page server action', () => {
it('returns fail(400) when password is empty', async () => {
const url = createUrl('/reset-password', { token: 'token-1' });
const result = await invokeDefault(
url,
createFormRequest({ password: '', password2: '' })
);
const result = await invokeDefault(url, createFormRequest({ password: '', password2: '' }));
expect(isActionFailure(result)).toBe(true);
if (isActionFailure(result)) {
expect(result.data).toEqual({ error: 'Password must be at least 8 characters.' });
@@ -181,10 +177,7 @@ describe('reset-password page server action', () => {
const url = createUrl('/reset-password', { token: 'token-1' });
const err = await Promise.resolve(
invokeDefault(
url,
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
)
invokeDefault(url, createFormRequest({ password: 'longenough1', password2: 'longenough1' }))
).catch((e: unknown) => e);
expect(isRedirect(err)).toBe(true);
if (isRedirect(err)) {
+11 -11
View File
@@ -2,11 +2,11 @@
* AUTHOR: Neil Clack
* EMAIL: nclack@radiantwavetech.com
* DATE: 07/06/2027
*
*
* An example endpoint, temporarily enabled, to describe
* how to send an email with the Resend API and our custom
* how to send an email with the Resend API and our custom
* sendMail function.
*
*
*/
import { json } from '@sveltejs/kit';
@@ -14,12 +14,12 @@ import { sendMail } from '$lib/server/resend';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => {
const { to, subject, templateId, data } = await request.json();
const { to, subject, templateId, data } = await request.json();
try {
const result = await sendMail({ to, subject, templateId, data });
return json(result);
} catch (e) {
return json({ error: (e as Error).message }, { status: 400 });
}
};
try {
const result = await sendMail({ to, subject, templateId, data });
return json(result);
} catch (e) {
return json({ error: (e as Error).message }, { status: 400 });
}
};