Compare commits

...

22 Commits

Author SHA1 Message Date
eewing fcd2c0342f Enhance receipt display by adding conditional rendering for descriptions and editing capabilities. Introduce a derived state for edit permissions and update UI components to reflect these changes, improving user interaction and information visibility.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m38s
2026-02-26 14:30:22 -06:00
eewing c89329dada Add receipt status management functionality, allowing users to update receipt statuses through a new server action. Introduce STATUS_OPTIONS for consistent status handling across components. Enhance UI to support status selection and display, improving overall user experience in receipt management.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m18s
2026-02-26 14:26:15 -06:00
eewing e3d96b8818 Enhance receipt management by adding a Description field to receipts, improving user input validation and error handling in the server actions. Update UI components to support the new Description field, ensuring a more comprehensive receipt creation and editing experience.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m49s
2026-02-26 11:10:58 -06:00
eewing b72df41ec7 Enhance image upload handling by integrating HEIC to JPEG conversion in receipts routes. Update ReceiptForm and related components to improve user feedback during image processing, ensuring compatibility with HEIC files. Refactor image handling logic for better maintainability and user experience.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m35s
2026-02-26 10:48:33 -06:00
eewing 694a5295cc Add receipt dialog functionality to +page.svelte for improved user experience
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m29s
2026-02-26 10:25:18 -06:00
eewing 39bea5e7a9 Refactor routing and component structure by removing transactions-related routes and components, consolidating login functionality across multiple nodes. Update type definitions and server load functions to reflect these changes, enhancing code maintainability and streamlining user navigation. Adjust UI components to focus on receipts, improving user experience.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m52s
2026-02-26 10:13:28 -06:00
eewing f8b8ecd88c Update server internal options to reflect a new version hash and refactor cookie handling in hooks.server.ts for improved PocketBase authentication management, enhancing response integrity and avoiding server errors.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m43s
2026-02-26 10:05:18 -06:00
eewing eb64dc8022 Remove navigateFallback option from Workbox configuration in vite.config.ts to streamline service worker setup.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m36s
2026-02-26 09:57:06 -06:00
eewing 455b762eef Refactor image upload handling across items and receipts routes to include validation for uploaded images, enhancing error handling and user feedback. Consolidate parsing functions by importing from a shared module, improving code maintainability. Update server hooks to ensure consistent cookie handling and error logging.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m33s
2026-02-26 09:45:37 -06:00
eewing 47625259ba Update UI components in +page.svelte and HomeWithDialog.svelte to disable certain buttons with a "Coming soon" label, enhancing user experience by indicating future functionality. Adjust server internal options to reflect a new version hash.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m28s
2026-02-26 09:20:43 -06:00
eewing 8b977b5e86 Refactor image conversion logic in convert-image-to-webp.ts to handle HEIC files and improve compatibility with Safari. Update ReceiptForm and +page components to streamline image upload process, removing camera functionality and enhancing user feedback during image processing. Adjust styles for better accessibility and clarity in image handling.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m50s
2026-02-19 15:07:32 -06:00
eewing 132e376b9f Implement camera functionality in ReceiptForm and +page components for capturing photos directly. Refactor image upload handling to support both camera and gallery options, enhancing user experience. Update styles for improved accessibility and feedback during image processing.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m29s
2026-02-19 11:52:23 -06:00
eewing c06daf4f7c Update package.json to allow Vite dev server to listen on all interfaces. Refactor dialog components in HomeWithDialog and receipts pages for improved layout and responsiveness. Enhance ReceiptForm styles for better user experience and accessibility, including adjustments to flex properties and overflow handling.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m38s
2026-02-19 11:44:00 -06:00
eewing d06e585bb5 Enhance image upload experience in ReceiptForm and +page components by integrating a loading state with a spinner icon during image processing. Update styles for better accessibility and user feedback, ensuring a smoother interaction when converting images to WebP format.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m33s
2026-02-19 11:33:57 -06:00
eewing 8af1ee8bd4 Refactor image upload handling in ReceiptForm and +page components by removing direct input element clicks and replacing them with accessible labels. Update styles for improved user experience and maintainability.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m42s
2026-02-19 11:27:56 -06:00
eewing 6e6ad0de05 Refactor receipt handling by removing status options from the server-side logic and simplifying the default status to "New". Update the receipt form to eliminate the status selection, enhancing user experience by focusing on essential fields. Improve image upload handling with WebP conversion support in both receipt creation and editing components.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m25s
2026-02-19 11:19:35 -06:00
eewing f51f9c9f81 Enhance receipt management by adding support for image uploads and improving data handling in receipt forms. Update route parameters and type definitions for better data structure. Refactor UI components for improved user experience and responsiveness, including collapsible sections for receipt images and dynamic line item management.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m42s
2026-02-19 10:57:15 -06:00
eewing 115eecc6f3 Refactor ItemsCrud.svelte to initialize searchQuery with an empty string and update it based on URL parameters using a reactive effect. Update receipt image handling in +page.svelte to define the image URL as a constant for improved readability.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m37s
2026-02-19 10:02:49 -06:00
eewing 567d7e3e13 Implement receipt management features including creation, loading, and display of receipts. Refactor related components and enhance UI with dialogs for adding new receipts. Update type definitions and improve error handling for better user experience.
CI / build (push) Has been skipped
CI / deploy (push) Failing after 1m15s
2026-02-19 09:55:26 -06:00
eewing c6b30100bd feat(sso): accept base-issued iframe login
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m44s
Handle Base SSO tokens on initial requests so Stackq can bootstrap PocketBase auth and redirect into the app without a separate login.
2026-02-19 09:36:08 -06:00
eewing e3ec0219b7 Update .DS_Store to reflect recent project changes.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m51s
2026-02-19 09:26:04 -06:00
eewing 73fcd791ba Update .DS_Store to reflect changes in project files. 2026-02-19 09:25:53 -06:00
114 changed files with 2083 additions and 13782 deletions
Vendored
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
.DS_Store
Thumbs.db
node_modules
/.svelte-kit
/build
/.output
.env
.env.*
!.env.example
!.env.test
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+3 -19
View File
@@ -8,34 +8,18 @@ export const nodes = [
() => import('./nodes/4'),
() => import('./nodes/5'),
() => import('./nodes/6'),
() => import('./nodes/7'),
() => import('./nodes/8'),
() => import('./nodes/9'),
() => import('./nodes/10'),
() => import('./nodes/11'),
() => import('./nodes/12'),
() => import('./nodes/13'),
() => import('./nodes/14'),
() => import('./nodes/15')
() => import('./nodes/7')
];
export const server_loads = [0];
export const dictionary = {
"/": [~2],
"/accounts": [~10],
"/accounts/new": [11],
"/accounts/[id]": [12],
"/items": [~13],
"/items/[id]": [~14],
"/login": [~15],
"/login": [~7],
"/logout": [~6],
"/receipts": [~3],
"/receipts/new": [4],
"/receipts/[id]": [5],
"/transactions": [~7],
"/transactions/new": [8],
"/transactions/[id]": [9]
"/receipts/[id]": [~5]
};
export const hooks = {
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/accounts/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/accounts/new/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/accounts/[id]/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/items/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/items/[id]/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/transactions/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/transactions/new/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+1 -1
View File
@@ -1 +1 @@
export { default as component } from "../../../../src/routes/transactions/[id]/+page.svelte";
export { default as component } from "../../../../src/routes/login/+page.svelte";
+2 -2
View File
@@ -21,10 +21,10 @@ export const options = {
service_worker: false,
service_worker_options: undefined,
templates: {
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='4' fill='%231e293b'/><text x='16' y='22' font-size='18' font-family='system-ui' fill='%2394a3b8' text-anchor='middle'>S</text></svg>\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover\" />\n <meta name=\"theme-color\" content=\"#0f172a\" />\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\n <meta name=\"apple-mobile-web-app-title\" content=\"Stackq\" />\n <link rel=\"apple-touch-icon\" href=\"/icon.svg\" />\n <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n " + head + "\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
app: ({ head, body, assets, nonce, env }) => "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" href=\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='4' fill='%231e293b'/><text x='16' y='22' font-size='18' font-family='system-ui' fill='%2394a3b8' text-anchor='middle'>S</text></svg>\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover\" />\n <meta name=\"theme-color\" content=\"#0f172a\" />\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\n <meta name=\"apple-mobile-web-app-title\" content=\"Stackq\" />\n <link rel=\"apple-touch-icon\" href=\"/icon.svg\" />\n <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n " + head + "\n </head>\n <body data-sveltekit-preload-data=\"hover\">\n <div style=\"display: contents\">" + body + "</div>\n </body>\n</html>\n",
error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
},
version_hash: "16sw9ck"
version_hash: "49o61d"
};
export async function get_hooks() {
+8 -5
View File
@@ -27,20 +27,23 @@ export {};
declare module "$app/types" {
export interface AppTypes {
RouteId(): "/" | "/access-denied" | "/accounts" | "/accounts/new" | "/accounts/[id]" | "/api" | "/api/toggle-darkmode" | "/items" | "/items/[id]" | "/login" | "/logout" | "/receipts" | "/receipts/new" | "/receipts/[id]" | "/transactions" | "/transactions/new" | "/transactions/[id]";
RouteId(): "/" | "/access-denied" | "/accounts" | "/accounts/new" | "/accounts/[id]" | "/api" | "/api/files" | "/api/files/[...path]" | "/api/toggle-darkmode" | "/items" | "/items/[id]" | "/login" | "/logout" | "/receipts" | "/receipts/new" | "/receipts/[id]" | "/transactions" | "/transactions/new" | "/transactions/[id]";
RouteParams(): {
"/accounts/[id]": { id: string };
"/api/files/[...path]": { path: string };
"/items/[id]": { id: string };
"/receipts/[id]": { id: string };
"/transactions/[id]": { id: string }
};
LayoutParams(): {
"/": { id?: string };
"/": { id?: string; path?: string };
"/access-denied": Record<string, never>;
"/accounts": { id?: string };
"/accounts/new": Record<string, never>;
"/accounts/[id]": { id: string };
"/api": Record<string, never>;
"/api": { path?: string };
"/api/files": { path?: string };
"/api/files/[...path]": { path: string };
"/api/toggle-darkmode": Record<string, never>;
"/items": { id?: string };
"/items/[id]": { id: string };
@@ -53,8 +56,8 @@ declare module "$app/types" {
"/transactions/new": Record<string, never>;
"/transactions/[id]": { id: string }
};
Pathname(): "/" | "/accounts" | "/accounts/new" | `/accounts/${string}` & {} | "/items" | `/items/${string}` & {} | "/login" | "/logout" | "/receipts" | "/receipts/new" | `/receipts/${string}` & {} | "/transactions" | "/transactions/new" | `/transactions/${string}` & {};
Pathname(): "/" | `/api/files/${string}` & {} | "/login" | "/logout" | "/receipts" | "/receipts/new" | `/receipts/${string}` & {};
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
Asset(): "/icon.svg" | string & {};
Asset(): "/icon.svg" | "/manifest.webmanifest" | string & {};
}
}
+3 -35
View File
@@ -6,28 +6,8 @@
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/accounts": [
"src/routes/accounts/+page.server.ts",
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/accounts/new": [
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/accounts/[id]": [
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/items": [
"src/routes/items/+page.server.ts",
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/items/[id]": [
"src/routes/items/[id]/+page.server.ts",
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
"/api/files/[...path]": [
"src/routes/api/files/[...path]/+server.ts"
],
"/login": [
"src/routes/login/+page.server.ts",
@@ -49,19 +29,7 @@
"src/routes/+layout.server.ts"
],
"/receipts/[id]": [
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/transactions": [
"src/routes/transactions/+page.server.ts",
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/transactions/new": [
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
],
"/transactions/[id]": [
"src/routes/receipts/[id]/+page.server.ts",
"src/routes/+layout.ts",
"src/routes/+layout.server.ts"
]
+1 -1
View File
@@ -13,7 +13,7 @@ type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<LayoutServerData>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | "/receipts" | "/receipts/new" | "/receipts/[id]" | "/logout" | "/transactions" | "/transactions/new" | "/transactions/[id]" | "/accounts" | "/accounts/new" | "/accounts/[id]" | "/items" | "/items/[id]" | "/login" | null
type LayoutRouteId = RouteId | "/" | "/receipts" | "/receipts/new" | "/receipts/[id]" | "/logout" | "/login" | null
type LayoutParams = RouteParams & { id?: string }
type LayoutServerParentData = EnsureDefined<{}>;
type LayoutParentData = EnsureDefined<{}>;
+3 -16
View File
@@ -11,21 +11,8 @@ type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Pa
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
+3 -16
View File
@@ -11,22 +11,9 @@ type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Pa
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
@@ -1,91 +0,0 @@
// @ts-nocheck
import { error, fail, redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
function parseNum(val: unknown): number | undefined {
if (val === "" || val === null || val === undefined) return undefined;
const n = Number(val);
return Number.isFinite(n) ? n : undefined;
}
function parseBool(val: unknown): boolean {
return val === "on" || val === "true" || val === true;
}
export const load = async ({ params, locals }: Parameters<PageServerLoad>[0]) => {
const id = params.id;
if (!id) throw error(404, "Item not found");
try {
const item = await locals.pb
.collection(COLLECTION)
.getOne<StackqItem>(id, { expand: "Parent" });
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqItem>({ sort: "-created", expand: "Parent" })
.catch(() => [] as StackqItem[]);
const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU }));
return { item, itemsForParent };
} catch {
throw error(404, "Item not found");
}
};
export const actions = {
update: async ({ request, locals, params }: import('./$types').RequestEvent) => {
const id = params.id;
if (!id) return fail(400, { error: "Missing id", form: "update" });
const form = await request.formData();
const Item = (form.get("Item") as string)?.trim() ?? "";
const SKU = (form.get("SKU") as string)?.trim() ?? "";
const Description = (form.get("Description") as string)?.trim() ?? "";
const UOM = (form.get("UOM") as string)?.trim() ?? "";
const Cost = parseNum(form.get("Cost"));
const Vendor = (form.get("Vendor") as string)?.trim() ?? "";
const Catagory = (form.get("Catagory") as string)?.trim() ?? "";
const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? "";
const Has_Parent = parseBool(form.get("Has_Parent"));
const Parent = (form.get("Parent") as string)?.trim() ?? "";
const Stock = parseBool(form.get("Stock"));
const Image = form.get("Image") as File | null;
const imageFile = Image && Image.size > 0 ? Image : undefined;
if (!Item) return fail(400, { error: "Item name is required", form: "update" });
try {
const body: Record<string, unknown> = {
Item,
SKU,
Description,
UOM: UOM || undefined,
Cost: Cost ?? 0,
Vendor,
Catagory: Catagory || undefined,
Sub_Catagory: Sub_Catagory || undefined,
Has_Parent: !!Has_Parent,
Parent: Parent || null,
Stock: !!Stock,
};
if (imageFile) body.Image = imageFile;
await locals.pb.collection(COLLECTION).update(id, body);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Update failed";
return fail(500, { error: message, form: "update" });
}
return { success: true };
},
delete: async ({ locals, params }: import('./$types').RequestEvent) => {
const id = params.id;
if (!id) return fail(400, { error: "Missing id", form: "delete" });
try {
await locals.pb.collection(COLLECTION).delete(id);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Delete failed";
return fail(500, { error: message, form: "delete" });
}
throw redirect(303, "/items");
},
};
;null as any as Actions;
@@ -1,134 +0,0 @@
// @ts-nocheck
import { fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
function parseNum(val: unknown): number | undefined {
if (val === "" || val === null || val === undefined) return undefined;
const n = Number(val);
return Number.isFinite(n) ? n : undefined;
}
function parseBool(val: unknown): boolean {
return val === "on" || val === "true" || val === true;
}
export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqItem>({
sort: "-created",
expand: "Parent",
})
.catch((err) => {
console.error(COLLECTION, err);
return [] as StackqItem[];
});
const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU }));
return { items: list, itemsForParent };
};
export const actions = {
create: async ({ request, locals }: import('./$types').RequestEvent) => {
const form = await request.formData();
const Item = (form.get("Item") as string)?.trim() ?? "";
const SKU = (form.get("SKU") as string)?.trim() ?? "";
const Description = (form.get("Description") as string)?.trim() ?? "";
const UOM = (form.get("UOM") as string)?.trim() ?? "";
const Cost = parseNum(form.get("Cost"));
const Vendor = (form.get("Vendor") as string)?.trim() ?? "";
const Catagory = (form.get("Catagory") as string)?.trim() ?? "";
const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? "";
const Has_Parent = parseBool(form.get("Has_Parent"));
const Parent = (form.get("Parent") as string)?.trim() ?? "";
const Stock = parseBool(form.get("Stock"));
const imageFileCreate = form.get("Image") as File | null;
const hasImage = imageFileCreate && imageFileCreate.size > 0;
if (!Item) return fail(400, { error: "Item name is required", form: "create" });
try {
const body: Record<string, unknown> = {
Item,
SKU,
Description,
UOM: UOM || undefined,
Cost: Cost ?? 0,
Vendor,
Catagory: Catagory || undefined,
Sub_Catagory: Sub_Catagory || undefined,
Has_Parent: !!Has_Parent,
Parent: Parent || null,
Stock: !!Stock,
};
if (hasImage) body.Image = imageFileCreate;
await locals.pb.collection(COLLECTION).create(body);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Create failed";
return fail(500, { error: message, form: "create" });
}
return { success: true };
},
update: async ({ request, locals }: import('./$types').RequestEvent) => {
const form = await request.formData();
const id = (form.get("id") as string)?.trim();
if (!id) return fail(400, { error: "Missing id", form: "update" });
const Item = (form.get("Item") as string)?.trim() ?? "";
const SKU = (form.get("SKU") as string)?.trim() ?? "";
const Description = (form.get("Description") as string)?.trim() ?? "";
const UOM = (form.get("UOM") as string)?.trim() ?? "";
const Cost = parseNum(form.get("Cost"));
const Vendor = (form.get("Vendor") as string)?.trim() ?? "";
const Catagory = (form.get("Catagory") as string)?.trim() ?? "";
const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? "";
const Has_Parent = parseBool(form.get("Has_Parent"));
const Parent = (form.get("Parent") as string)?.trim() ?? "";
const Stock = parseBool(form.get("Stock"));
const Image = form.get("Image") as File | null;
const imageFile = Image && Image.size > 0 ? Image : undefined;
if (!Item) return fail(400, { error: "Item name is required", form: "update" });
try {
const body: Record<string, unknown> = {
Item,
SKU,
Description,
UOM: UOM || undefined,
Cost: Cost ?? 0,
Vendor,
Catagory: Catagory || undefined,
Sub_Catagory: Sub_Catagory || undefined,
Has_Parent: !!Has_Parent,
Parent: Parent || null,
Stock: !!Stock,
};
if (imageFile) body.Image = imageFile;
await locals.pb.collection(COLLECTION).update(id, body);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Update failed";
return fail(500, { error: message, form: "update" });
}
return { success: true };
},
delete: async ({ request, locals }: import('./$types').RequestEvent) => {
const form = await request.formData();
const id = (form.get("id") as string)?.trim();
if (!id) return fail(400, { error: "Missing id", form: "delete" });
try {
await locals.pb.collection(COLLECTION).delete(id);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Delete failed";
return fail(500, { error: message, form: "delete" });
}
return { success: true };
},
};
;null as any as Actions;
@@ -1,17 +1,8 @@
// @ts-nocheck
import type { Actions, PageServerLoad } from "./$types";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqItem>({ sort: "-created", expand: "Parent" })
.catch(() => [] as StackqItem[]);
const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU }));
return { items: list, itemsForParent };
export const load = async () => {
return {};
};
export const actions = {
@@ -31,4 +22,4 @@ export const actions = {
}
},
};
;null as any as Actions;
;null as any as PageServerLoad;;null as any as Actions;
+7 -1
View File
@@ -16,7 +16,13 @@ type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
export type ActionData = unknown;
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
+16 -3
View File
@@ -11,9 +11,22 @@ type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Pa
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
@@ -1,8 +1,108 @@
// @ts-nocheck
import type { PageServerLoad } from "./$types";
import { fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import type { StackqReceipt } from "$lib/types/receipts";
import { STATUS_OPTIONS } from "$lib/types/receipts";
import { convertHeicFileToJpegIfNeeded } from "$lib/convert-heic-server";
import { validateImageUpload } from "$lib/validate-image-upload";
export const load = async () => {
// Placeholder: no PocketBase receipts collection yet. Return empty list.
return { receipts: [] as { id: string }[] };
const COLLECTION = "Stackq_Receipts";
const TYPE_OPTIONS = ["Purchase", "Gas"] as const;
export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
const user = locals.user as { id: string; Admin?: boolean } | null;
if (!user?.id) return { receipts: [] as StackqReceipt[] };
const isAdmin = user.Admin === true;
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqReceipt>({
sort: "-created",
expand: "User",
...(isAdmin ? {} : { filter: `User = "${user.id}"` }),
})
.catch((err) => {
console.error(COLLECTION, err);
return [] as StackqReceipt[];
});
return { receipts: list, canEdit: isAdmin };
};
;null as any as PageServerLoad;
export const actions = {
create: async ({ request, locals }: import('./$types').RequestEvent) => {
const user = locals.user;
if (!user?.id) return fail(401, { error: "You must be logged in to create a receipt." });
const form = await request.formData();
const Type = (form.get("Type") as string)?.trim() || "Purchase";
const Description = (form.get("Description") as string)?.trim() ?? "";
if (!Description) {
return fail(400, { error: "Description is required.", Type });
}
const imageFile = form.get("Image") as File | null;
const hasImage = imageFile && imageFile.size > 0;
if (hasImage) {
const imgError = validateImageUpload(imageFile);
if (imgError) return fail(400, { error: imgError, Type, Description });
}
let imageToSave: File | null = hasImage ? imageFile : null;
if (imageToSave) {
try {
imageToSave = await convertHeicFileToJpegIfNeeded(imageToSave);
} catch {
return fail(400, {
error: "Could not convert HEIC image. Try saving as JPEG (iPhone: Settings → Camera → Formats) or use a different photo.",
Type,
Description,
});
}
}
const body: Record<string, unknown> = {
Status: "New",
Type: TYPE_OPTIONS.includes(Type as (typeof TYPE_OPTIONS)[number]) ? Type : "Purchase",
Description,
User: user.id,
};
if (imageToSave) body.Image = imageToSave;
try {
await locals.pb.collection(COLLECTION).create(body);
} catch (err: unknown) {
const message =
err && typeof err === "object" && "message" in err
? String((err as { message: string }).message)
: "Failed to create receipt.";
return fail(500, { error: message, Type, Description });
}
return { success: true };
},
setStatus: async ({ request, locals }: import('./$types').RequestEvent) => {
const user = locals.user as { id: string; Admin?: boolean } | null;
if (!user?.id) return fail(401, { error: "Not logged in", setStatus: null });
const form = await request.formData();
const id = (form.get("id") as string)?.trim();
const Status = (form.get("Status") as string)?.trim() || "New";
if (!id) return fail(400, { error: "Missing receipt id", setStatus: null });
if (!STATUS_OPTIONS.includes(Status as (typeof STATUS_OPTIONS)[number])) {
return fail(400, { error: "Invalid status", setStatus: null });
}
const isAdmin = user.Admin === true;
if (!isAdmin) return fail(403, { error: "Only admins can change status.", setStatus: null });
try {
await locals.pb.collection(COLLECTION).getOne<StackqReceipt>(id);
await locals.pb.collection(COLLECTION).update(id, { Status });
} catch (e: unknown) {
const is404 = e && typeof e === "object" && "status" in e && (e as { status: number }).status === 404;
if (is404) return fail(404, { error: "Receipt not found", setStatus: null });
return fail(500, { error: "Failed to update status", setStatus: null });
}
return { success: true, setStatus: id };
},
};
;null as any as Actions;
+3 -10
View File
@@ -11,15 +11,8 @@ type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Pa
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
export type ActionData = unknown;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type PageProps = { params: RouteParams; data: PageData }
@@ -1,7 +0,0 @@
// @ts-nocheck
import type { PageServerLoad } from "./$types";
export const load = async () => {
return { transactions: [] as { id: string }[] };
};
;null as any as PageServerLoad;
+14
View File
@@ -7,6 +7,8 @@
"dependencies": {
"barcode-detector-api-polyfill": "^1.0.20",
"clsx": "^2.1.1",
"heic-convert": "^2.1.0",
"heic2any": "^0.0.4",
"pocketbase": "^0.26.0",
"tailwind-merge": "^3.0.0",
},
@@ -595,6 +597,12 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"heic-convert": ["heic-convert@2.1.0", "", { "dependencies": { "heic-decode": "^2.0.0", "jpeg-js": "^0.4.4", "pngjs": "^6.0.0" } }, "sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg=="],
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
"heic2any": ["heic2any@0.0.4", "", {}, "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="],
"idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="],
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
@@ -667,6 +675,8 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -687,6 +697,8 @@
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
"libheif-js": ["libheif-js@1.19.8", "", {}, "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
@@ -761,6 +773,8 @@
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
"pocketbase": ["pocketbase@0.26.8", "", {}, "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+7 -7
View File
@@ -1,6 +1,7 @@
import {
Icon_default
} from "./chunk-6EUZYYI5.js";
} from "./chunk-V6DNLLKF.js";
import "./chunk-2LGM3QYM.js";
import {
check_target,
hmr,
@@ -9,12 +10,12 @@ import {
snippet,
spread_props,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -23,12 +24,11 @@ import {
noop,
pop,
push
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/@lucide/svelte/dist/icons/check.svelte
Check[FILENAME] = "node_modules/@lucide/svelte/dist/icons/check.svelte";
+7 -7
View File
@@ -1,6 +1,7 @@
import {
Icon_default
} from "./chunk-6EUZYYI5.js";
} from "./chunk-V6DNLLKF.js";
import "./chunk-2LGM3QYM.js";
import {
check_target,
hmr,
@@ -9,12 +10,12 @@ import {
snippet,
spread_props,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -23,12 +24,11 @@ import {
noop,
pop,
push
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/@lucide/svelte/dist/icons/chevron-right.svelte
Chevron_right[FILENAME] = "node_modules/@lucide/svelte/dist/icons/chevron-right.svelte";
+7 -7
View File
@@ -1,6 +1,7 @@
import {
Icon_default
} from "./chunk-6EUZYYI5.js";
} from "./chunk-V6DNLLKF.js";
import "./chunk-2LGM3QYM.js";
import {
check_target,
hmr,
@@ -9,12 +10,12 @@ import {
snippet,
spread_props,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -23,12 +24,11 @@ import {
noop,
pop,
push
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/@lucide/svelte/dist/icons/circle.svelte
Circle[FILENAME] = "node_modules/@lucide/svelte/dist/icons/circle.svelte";
+7 -7
View File
@@ -1,6 +1,7 @@
import {
Icon_default
} from "./chunk-6EUZYYI5.js";
} from "./chunk-V6DNLLKF.js";
import "./chunk-2LGM3QYM.js";
import {
check_target,
hmr,
@@ -9,12 +10,12 @@ import {
snippet,
spread_props,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -23,12 +24,11 @@ import {
noop,
pop,
push
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/@lucide/svelte/dist/icons/minus.svelte
Minus[FILENAME] = "node_modules/@lucide/svelte/dist/icons/minus.svelte";
+7 -7
View File
@@ -1,6 +1,7 @@
import {
Icon_default
} from "./chunk-6EUZYYI5.js";
} from "./chunk-V6DNLLKF.js";
import "./chunk-2LGM3QYM.js";
import {
check_target,
hmr,
@@ -9,12 +10,12 @@ import {
snippet,
spread_props,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -23,12 +24,11 @@ import {
noop,
pop,
push
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/@lucide/svelte/dist/icons/x.svelte
X[FILENAME] = "node_modules/@lucide/svelte/dist/icons/x.svelte";
+84 -60
View File
@@ -1,229 +1,247 @@
{
"hash": "6372abc7",
"hash": "e1b6cade",
"configHash": "441370a6",
"lockfileHash": "43c145e9",
"browserHash": "583ba445",
"lockfileHash": "9401e68b",
"browserHash": "8eb13a4f",
"optimized": {
"svelte": {
"src": "../../svelte/src/index-client.js",
"file": "svelte.js",
"fileHash": "b620c7b2",
"fileHash": "07b115b9",
"needsInterop": false
},
"svelte/animate": {
"src": "../../svelte/src/animate/index.js",
"file": "svelte_animate.js",
"fileHash": "02f610c1",
"fileHash": "c012b289",
"needsInterop": false
},
"svelte/attachments": {
"src": "../../svelte/src/attachments/index.js",
"file": "svelte_attachments.js",
"fileHash": "aa2fd54c",
"fileHash": "6bded66c",
"needsInterop": false
},
"svelte/easing": {
"src": "../../svelte/src/easing/index.js",
"file": "svelte_easing.js",
"fileHash": "0f0d888b",
"fileHash": "8c8820c0",
"needsInterop": false
},
"svelte/internal": {
"src": "../../svelte/src/internal/index.js",
"file": "svelte_internal.js",
"fileHash": "ae1c44b6",
"fileHash": "adeb43af",
"needsInterop": true
},
"svelte/internal/client": {
"src": "../../svelte/src/internal/client/index.js",
"file": "svelte_internal_client.js",
"fileHash": "9548cc44",
"fileHash": "c34b6da3",
"needsInterop": false
},
"svelte/internal/disclose-version": {
"src": "../../svelte/src/internal/disclose-version.js",
"file": "svelte_internal_disclose-version.js",
"fileHash": "523d7d1e",
"fileHash": "9a06bc8d",
"needsInterop": false
},
"svelte/internal/flags/async": {
"src": "../../svelte/src/internal/flags/async.js",
"file": "svelte_internal_flags_async.js",
"fileHash": "69240c49",
"fileHash": "61336ace",
"needsInterop": false
},
"svelte/internal/flags/legacy": {
"src": "../../svelte/src/internal/flags/legacy.js",
"file": "svelte_internal_flags_legacy.js",
"fileHash": "4a2d0680",
"fileHash": "b5bf1ecd",
"needsInterop": false
},
"svelte/internal/flags/tracing": {
"src": "../../svelte/src/internal/flags/tracing.js",
"file": "svelte_internal_flags_tracing.js",
"fileHash": "65275630",
"fileHash": "3fd00f44",
"needsInterop": false
},
"svelte/legacy": {
"src": "../../svelte/src/legacy/legacy-client.js",
"file": "svelte_legacy.js",
"fileHash": "753552cd",
"fileHash": "3de6d8ec",
"needsInterop": false
},
"svelte/motion": {
"src": "../../svelte/src/motion/index.js",
"file": "svelte_motion.js",
"fileHash": "1405f1f5",
"fileHash": "d56675c0",
"needsInterop": false
},
"svelte/reactivity": {
"src": "../../svelte/src/reactivity/index-client.js",
"file": "svelte_reactivity.js",
"fileHash": "a6ac2ac8",
"fileHash": "699399e6",
"needsInterop": false
},
"svelte/reactivity/window": {
"src": "../../svelte/src/reactivity/window/index.js",
"file": "svelte_reactivity_window.js",
"fileHash": "25337be5",
"fileHash": "a0847fd2",
"needsInterop": false
},
"svelte/store": {
"src": "../../svelte/src/store/index-client.js",
"file": "svelte_store.js",
"fileHash": "0bb2bc3e",
"fileHash": "14d7ddb4",
"needsInterop": false
},
"svelte/transition": {
"src": "../../svelte/src/transition/index.js",
"file": "svelte_transition.js",
"fileHash": "0c13897c",
"fileHash": "423bf8f4",
"needsInterop": false
},
"svelte/events": {
"src": "../../svelte/src/events/index.js",
"file": "svelte_events.js",
"fileHash": "8e64c629",
"fileHash": "078c4a8c",
"needsInterop": false
},
"svelte > clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "svelte___clsx.js",
"fileHash": "a1e066f0",
"fileHash": "1ee833c7",
"needsInterop": false
},
"@lucide/svelte/icons/check": {
"src": "../../@lucide/svelte/dist/icons/check.js",
"file": "@lucide_svelte_icons_check.js",
"fileHash": "891b74d4",
"fileHash": "0ac209ae",
"needsInterop": false
},
"@lucide/svelte/icons/chevron-right": {
"src": "../../@lucide/svelte/dist/icons/chevron-right.js",
"file": "@lucide_svelte_icons_chevron-right.js",
"fileHash": "63648db9",
"fileHash": "c09b9f4f",
"needsInterop": false
},
"@lucide/svelte/icons/circle": {
"src": "../../@lucide/svelte/dist/icons/circle.js",
"file": "@lucide_svelte_icons_circle.js",
"fileHash": "11e4822b",
"fileHash": "e48abf24",
"needsInterop": false
},
"@lucide/svelte/icons/minus": {
"src": "../../@lucide/svelte/dist/icons/minus.js",
"file": "@lucide_svelte_icons_minus.js",
"fileHash": "30cf92e4",
"fileHash": "cbdf943d",
"needsInterop": false
},
"@lucide/svelte/icons/x": {
"src": "../../@lucide/svelte/dist/icons/x.js",
"file": "@lucide_svelte_icons_x.js",
"fileHash": "aa690a02",
"fileHash": "32d222b0",
"needsInterop": false
},
"barcode-detector-api-polyfill": {
"src": "../../barcode-detector-api-polyfill/esm/index.js",
"file": "barcode-detector-api-polyfill.js",
"fileHash": "55497bad",
"fileHash": "44de5b82",
"needsInterop": false
},
"bits-ui": {
"src": "../../bits-ui/dist/index.js",
"file": "bits-ui.js",
"fileHash": "1d3c3c46",
"fileHash": "9adf7bd3",
"needsInterop": false
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "b6a14292",
"fileHash": "6547c390",
"needsInterop": false
},
"devalue": {
"src": "../../devalue/index.js",
"file": "devalue.js",
"fileHash": "2ff911fe",
"fileHash": "6bd276fd",
"needsInterop": false
},
"esm-env": {
"src": "../../esm-env/index.js",
"file": "esm-env.js",
"fileHash": "5a410efb",
"fileHash": "a16abac7",
"needsInterop": false
},
"pocketbase": {
"src": "../../pocketbase/dist/pocketbase.es.mjs",
"file": "pocketbase.js",
"fileHash": "9e68746a",
"fileHash": "241d5081",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "d8639500",
"fileHash": "7d5f8442",
"needsInterop": false
},
"tailwind-variants": {
"src": "../../tailwind-variants/dist/index.js",
"file": "tailwind-variants.js",
"fileHash": "cf2013f5",
"fileHash": "3821161a",
"needsInterop": false
},
"@lucide/svelte/icons/chevron-down": {
"src": "../../@lucide/svelte/dist/icons/chevron-down.js",
"file": "@lucide_svelte_icons_chevron-down.js",
"fileHash": "48902207",
"needsInterop": false
},
"@lucide/svelte/icons/chevron-up": {
"src": "../../@lucide/svelte/dist/icons/chevron-up.js",
"file": "@lucide_svelte_icons_chevron-up.js",
"fileHash": "b33cd26c",
"needsInterop": false
},
"heic2any": {
"src": "../../heic2any/dist/heic2any.js",
"file": "heic2any.js",
"fileHash": "4e438dea",
"needsInterop": true
},
"@lucide/svelte/icons/loader-2": {
"src": "../../@lucide/svelte/dist/icons/loader-2.js",
"file": "@lucide_svelte_icons_loader-2.js",
"fileHash": "7eba973c",
"needsInterop": false
}
},
"chunks": {
"chunk-6EUZYYI5": {
"file": "chunk-6EUZYYI5.js"
"chunk-N4HEU72H": {
"file": "chunk-N4HEU72H.js"
},
"chunk-HRPYXN4O": {
"file": "chunk-HRPYXN4O.js"
},
"chunk-MZVN5SDE": {
"file": "chunk-MZVN5SDE.js"
},
"chunk-EZEETTA4": {
"file": "chunk-EZEETTA4.js"
},
"chunk-3TACVO2P": {
"file": "chunk-3TACVO2P.js"
"chunk-MPD7DSJG": {
"file": "chunk-MPD7DSJG.js"
},
"chunk-7RQDXF5S": {
"file": "chunk-7RQDXF5S.js"
},
"chunk-YERFD2CZ": {
"file": "chunk-YERFD2CZ.js"
"chunk-V6DNLLKF": {
"file": "chunk-V6DNLLKF.js"
},
"chunk-EXK7SDUS": {
"file": "chunk-EXK7SDUS.js"
"chunk-2LGM3QYM": {
"file": "chunk-2LGM3QYM.js"
},
"chunk-NTAG77ZX": {
"file": "chunk-NTAG77ZX.js"
},
"chunk-MKTP2BFS": {
"file": "chunk-MKTP2BFS.js"
},
"chunk-U7P2NEEE": {
"file": "chunk-U7P2NEEE.js"
},
"chunk-57IKBECK": {
"file": "chunk-57IKBECK.js"
},
"chunk-JC3VXQM7": {
"file": "chunk-JC3VXQM7.js"
"chunk-HZ6NBROY": {
"file": "chunk-HZ6NBROY.js"
},
"chunk-XWATFG4W": {
"file": "chunk-XWATFG4W.js"
@@ -231,14 +249,20 @@
"chunk-HNWPC2PS": {
"file": "chunk-HNWPC2PS.js"
},
"chunk-2LGM3QYM": {
"file": "chunk-2LGM3QYM.js"
"chunk-MZVN5SDE": {
"file": "chunk-MZVN5SDE.js"
},
"chunk-OHYQYV5R": {
"file": "chunk-OHYQYV5R.js"
},
"chunk-X4VJQ2O3": {
"file": "chunk-X4VJQ2O3.js"
"chunk-HRPYXN4O": {
"file": "chunk-HRPYXN4O.js"
},
"chunk-YERFD2CZ": {
"file": "chunk-YERFD2CZ.js"
},
"chunk-LYA4E26T": {
"file": "chunk-LYA4E26T.js"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import {
__publicField
} from "./chunk-X4VJQ2O3.js";
} from "./chunk-LYA4E26T.js";
// node_modules/ts-custom-error/dist/custom-error.mjs
function fixProto(target, prototype) {
+11 -11
View File
@@ -1,9 +1,9 @@
import "./chunk-MZVN5SDE.js";
import {
SvelteMap
} from "./chunk-EZEETTA4.js";
import "./chunk-3TACVO2P.js";
} from "./chunk-N4HEU72H.js";
import "./chunk-MPD7DSJG.js";
import "./chunk-7RQDXF5S.js";
import "./chunk-2LGM3QYM.js";
import {
add_locations,
append_styles,
@@ -38,10 +38,7 @@ import {
validate_snippet_args,
validate_void_dynamic_element,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import {
clsx
} from "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment,
@@ -52,7 +49,10 @@ import {
set_text,
text,
unmount
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import {
clsx
} from "./chunk-U7P2NEEE.js";
import {
FILENAME,
HMR,
@@ -88,12 +88,12 @@ import {
user_derived,
user_effect,
user_pre_effect
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import {
true_default
} from "./chunk-HNWPC2PS.js";
import "./chunk-2LGM3QYM.js";
import "./chunk-MZVN5SDE.js";
import "./chunk-OHYQYV5R.js";
import {
__export,
@@ -103,7 +103,7 @@ import {
__privateSet,
__privateWrapper,
__publicField
} from "./chunk-X4VJQ2O3.js";
} from "./chunk-LYA4E26T.js";
// node_modules/bits-ui/dist/bits/accordion/exports.js
var exports_exports = {};
-35
View File
@@ -1,35 +0,0 @@
import {
createSubscriber
} from "./chunk-JC3VXQM7.js";
import {
__privateAdd,
__privateGet,
__privateSet
} from "./chunk-X4VJQ2O3.js";
// node_modules/svelte/src/reactivity/reactive-value.js
var _fn, _subscribe;
var ReactiveValue = class {
/**
*
* @param {() => T} fn
* @param {(update: () => void) => void} onsubscribe
*/
constructor(fn, onsubscribe) {
__privateAdd(this, _fn);
__privateAdd(this, _subscribe);
__privateSet(this, _fn, fn);
__privateSet(this, _subscribe, createSubscriber(onsubscribe));
}
get current() {
__privateGet(this, _subscribe).call(this);
return __privateGet(this, _fn).call(this);
}
};
_fn = new WeakMap();
_subscribe = new WeakMap();
export {
ReactiveValue
};
//# sourceMappingURL=chunk-3TACVO2P.js.map
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": ["../../svelte/src/reactivity/reactive-value.js"],
"sourcesContent": ["import { createSubscriber } from './create-subscriber.js';\n\n/**\n * @template T\n */\nexport class ReactiveValue {\n\t#fn;\n\t#subscribe;\n\n\t/**\n\t *\n\t * @param {() => T} fn\n\t * @param {(update: () => void) => void} onsubscribe\n\t */\n\tconstructor(fn, onsubscribe) {\n\t\tthis.#fn = fn;\n\t\tthis.#subscribe = createSubscriber(onsubscribe);\n\t}\n\n\tget current() {\n\t\tthis.#subscribe();\n\t\treturn this.#fn();\n\t}\n}\n"],
"mappings": ";;;;;;;;;;AAAA;AAKO,IAAM,gBAAN,MAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS1B,YAAY,IAAI,aAAa;AAR7B;AACA;AAQC,uBAAK,KAAM;AACX,uBAAK,YAAa,iBAAiB,WAAW;AAAA,EAC/C;AAAA,EAEA,IAAI,UAAU;AACb,uBAAK,YAAL;AACA,WAAO,mBAAK,KAAL;AAAA,EACR;AACD;AAjBC;AACA;",
"names": []
}
-1017
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-188
View File
@@ -1,188 +0,0 @@
import {
add_locations,
attribute_effect,
check_target,
each,
element,
hmr,
index,
legacy_api,
prop,
rest_props,
snippet,
validate_dynamic_element_tag
} from "./chunk-EXK7SDUS.js";
import {
append,
comment,
from_svg
} from "./chunk-57IKBECK.js";
import {
FILENAME,
HMR,
add_svelte_meta,
child,
first_child,
get,
noop,
pop,
push,
reset,
sibling,
to_array,
user_derived
} from "./chunk-JC3VXQM7.js";
// node_modules/@lucide/svelte/dist/defaultAttributes.js
var defaultAttributes = {
xmlns: "http://www.w3.org/2000/svg",
width: 24,
height: 24,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": 2,
"stroke-linecap": "round",
"stroke-linejoin": "round"
};
var defaultAttributes_default = defaultAttributes;
// node_modules/@lucide/svelte/dist/Icon.svelte
Icon[FILENAME] = "node_modules/@lucide/svelte/dist/Icon.svelte";
var root = add_locations(from_svg(`<svg><!><!></svg>`), Icon[FILENAME], [[5, 0]]);
function Icon($$anchor, $$props) {
check_target(new.target);
push($$props, true, Icon);
const color = prop($$props, "color", 3, "currentColor"), size = prop($$props, "size", 3, 24), strokeWidth = prop($$props, "strokeWidth", 3, 2), absoluteStrokeWidth = prop($$props, "absoluteStrokeWidth", 3, false), iconNode = prop($$props, "iconNode", 19, () => []), props = rest_props(
$$props,
[
"$$slots",
"$$events",
"$$legacy",
"name",
"color",
"size",
"strokeWidth",
"absoluteStrokeWidth",
"iconNode",
"children"
],
"props"
);
var $$exports = { ...legacy_api() };
var svg = root();
attribute_effect(
svg,
($0) => ({
...defaultAttributes_default,
...props,
width: size(),
height: size(),
stroke: color(),
"stroke-width": $0,
class: [
"lucide-icon lucide",
$$props.name && `lucide-${$$props.name}`,
$$props.class
]
}),
[
() => absoluteStrokeWidth() ? Number(strokeWidth()) * 24 / Number(size()) : strokeWidth()
]
);
var node = child(svg);
add_svelte_meta(
() => each(node, 17, iconNode, index, ($$anchor2, $$item) => {
var $$array = user_derived(() => to_array(get($$item), 2));
let tag = () => get($$array)[0];
tag();
let attrs = () => get($$array)[1];
attrs();
var fragment = comment();
var node_1 = first_child(fragment);
{
validate_dynamic_element_tag(tag);
element(
node_1,
tag,
true,
($$element, $$anchor3) => {
attribute_effect($$element, () => ({ ...attrs() }));
},
void 0,
[15, 4]
);
}
append($$anchor2, fragment);
}),
"each",
Icon,
14,
2
);
var node_2 = sibling(node);
add_svelte_meta(() => snippet(node_2, () => $$props.children ?? noop), "render", Icon, 20, 2);
reset(svg);
append($$anchor, svg);
return pop($$exports);
}
if (import.meta.hot) {
Icon = hmr(Icon);
import.meta.hot.accept((module) => {
Icon[HMR].update(module.default);
});
}
var Icon_default = Icon;
export {
Icon_default
};
/*! Bundled license information:
@lucide/svelte/dist/defaultAttributes.js:
(**
* @license @lucide/svelte v0.561.0 - ISC
*
* ISC License
*
* Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* ---
*
* The MIT License (MIT) (for portions derived from Feather)
*
* Copyright (c) 2013-2023 Cole Bemis
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*)
*/
//# sourceMappingURL=chunk-6EUZYYI5.js.map
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": ["../../@lucide/svelte/dist/defaultAttributes.js", "../../@lucide/svelte/dist/Icon.svelte"],
"sourcesContent": ["/**\n * @license @lucide/svelte v0.561.0 - ISC\n *\n * ISC License\n * \n * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.\n * \n * Permission to use, copy, modify, and/or distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n * \n * THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n * \n * ---\n * \n * The MIT License (MIT) (for portions derived from Feather)\n * \n * Copyright (c) 2013-2023 Cole Bemis\n * \n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n * \n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n * \n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n * \n */\nconst defaultAttributes = {\n xmlns: 'http://www.w3.org/2000/svg',\n width: 24,\n height: 24,\n viewBox: '0 0 24 24',\n fill: 'none',\n stroke: 'currentColor',\n 'stroke-width': 2,\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n};\nexport default defaultAttributes;\n", "<script lang=\"ts\">import defaultAttributes from './defaultAttributes.js';\nconst { name, color = 'currentColor', size = 24, strokeWidth = 2, absoluteStrokeWidth = false, iconNode = [], children, ...props } = $props();\n</script>\n\n<svg\n {...defaultAttributes}\n {...props}\n width={size}\n height={size}\n stroke={color}\n stroke-width={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}\n class={['lucide-icon lucide', name && `lucide-${name}`, props.class]}\n>\n {#each iconNode as [tag, attrs]}\n <svelte:element\n this={tag}\n {...attrs}\n />\n {/each}\n {@render children?.()}\n</svg>\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,IAAM,oBAAoB;AAAA,EACtB,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,mBAAmB;AACvB;AACA,IAAO,4BAAQ;;;;;iCCvDf;;;QACc,QAAK,KAAA,SAAA,SAAA,GAAG,cAAc,GAAE,OAAI,KAAA,SAAA,QAAA,GAAG,EAAE,GAAE,cAAW,KAAA,SAAA,eAAA,GAAG,CAAC,GAAE,sBAAmB,KAAA,SAAA,uBAAA,GAAG,KAAK,GAAE,WAAQ,KAAA,SAAA,YAAA,IAAA,MAAA,CAAA,CAAA,GAAoB,QAAK;;;;;;;;;;;;;;;;;MAG/H,MAAA,KAAA;;IAAA;;SACK;SACA;aACG,KAAI;cACH,KAAI;cACJ,MAAK;;;QAEL;;gBAAsD;;;;YADhD,oBAAmB,IAAI,OAAO,YAAW,CAAA,IAAI,KAAM,OAAO,KAAI,CAAA,IAAI,YAAW;;;mBAN5F,GAAA;;yBASQ,UAAQ,OAAA,CAAAA,WAAA,WAAA;;UAAK,MAAI,MAAA,IAAA,OAAA,EAAA,CAAA;;UAAC,QAAM,MAAA,IAAA,OAAA,EAAA,CAAA;;;;;qCAErB,GAAG;;;UAAH;;;oDACF,MAAK,EAAA,EAAA;;;;;;;;;;;;;;;QAZd,GAAA;mBAAA,GAAA;;AAFO;;;;;;;;",
"names": ["$$anchor"]
}
-4576
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-736
View File
@@ -1,736 +0,0 @@
import {
ReactiveValue
} from "./chunk-3TACVO2P.js";
import {
active_reaction,
get,
increment,
label,
on,
set,
set_active_reaction,
source,
state,
tag,
update_version,
user_derived
} from "./chunk-JC3VXQM7.js";
import {
true_default
} from "./chunk-HNWPC2PS.js";
import {
__privateAdd,
__privateGet,
__privateMethod,
__privateSet,
__superGet
} from "./chunk-X4VJQ2O3.js";
// node_modules/svelte/src/reactivity/date.js
var inited = false;
var _time, _deriveds, _reaction, _SvelteDate_instances, init_fn;
var _SvelteDate = class _SvelteDate extends Date {
/** @param {any[]} params */
constructor(...params) {
super(...params);
__privateAdd(this, _SvelteDate_instances);
__privateAdd(this, _time, state(super.getTime()));
/** @type {Map<keyof Date, Source<unknown>>} */
__privateAdd(this, _deriveds, /* @__PURE__ */ new Map());
__privateAdd(this, _reaction, active_reaction);
if (true_default) {
tag(__privateGet(this, _time), "SvelteDate.#time");
}
if (!inited) __privateMethod(this, _SvelteDate_instances, init_fn).call(this);
}
};
_time = new WeakMap();
_deriveds = new WeakMap();
_reaction = new WeakMap();
_SvelteDate_instances = new WeakSet();
init_fn = function() {
inited = true;
var proto = _SvelteDate.prototype;
var date_proto = Date.prototype;
var methods = (
/** @type {Array<keyof Date & string>} */
Object.getOwnPropertyNames(date_proto)
);
for (const method of methods) {
if (method.startsWith("get") || method.startsWith("to") || method === "valueOf") {
proto[method] = function(...args) {
if (args.length > 0) {
get(__privateGet(this, _time));
return date_proto[method].apply(this, args);
}
var d = __privateGet(this, _deriveds).get(method);
if (d === void 0) {
const reaction = active_reaction;
set_active_reaction(__privateGet(this, _reaction));
d = user_derived(() => {
get(__privateGet(this, _time));
return date_proto[method].apply(this, args);
});
__privateGet(this, _deriveds).set(method, d);
set_active_reaction(reaction);
}
return get(d);
};
}
if (method.startsWith("set")) {
proto[method] = function(...args) {
var result = date_proto[method].apply(this, args);
set(__privateGet(this, _time), date_proto.getTime.call(this));
return result;
};
}
}
};
var SvelteDate = _SvelteDate;
// node_modules/svelte/src/reactivity/set.js
var read_methods = ["forEach", "isDisjointFrom", "isSubsetOf", "isSupersetOf"];
var set_like_methods = ["difference", "intersection", "symmetricDifference", "union"];
var inited2 = false;
var _sources, _version, _size, _update_version, _SvelteSet_instances, source_fn, init_fn2;
var _SvelteSet = class _SvelteSet extends Set {
/**
* @param {Iterable<T> | null | undefined} [value]
*/
constructor(value) {
super();
__privateAdd(this, _SvelteSet_instances);
/** @type {Map<T, Source<boolean>>} */
__privateAdd(this, _sources, /* @__PURE__ */ new Map());
__privateAdd(this, _version, state(0));
__privateAdd(this, _size, state(0));
__privateAdd(this, _update_version, update_version || -1);
if (true_default) {
value = new Set(value);
tag(__privateGet(this, _version), "SvelteSet version");
tag(__privateGet(this, _size), "SvelteSet.size");
}
if (value) {
for (var element of value) {
super.add(element);
}
__privateGet(this, _size).v = super.size;
}
if (!inited2) __privateMethod(this, _SvelteSet_instances, init_fn2).call(this);
}
/** @param {T} value */
has(value) {
var has = super.has(value);
var sources = __privateGet(this, _sources);
var s = sources.get(value);
if (s === void 0) {
if (!has) {
get(__privateGet(this, _version));
return false;
}
s = __privateMethod(this, _SvelteSet_instances, source_fn).call(this, true);
if (true_default) {
tag(s, `SvelteSet has(${label(value)})`);
}
sources.set(value, s);
}
get(s);
return has;
}
/** @param {T} value */
add(value) {
if (!super.has(value)) {
super.add(value);
set(__privateGet(this, _size), super.size);
increment(__privateGet(this, _version));
}
return this;
}
/** @param {T} value */
delete(value) {
var deleted = super.delete(value);
var sources = __privateGet(this, _sources);
var s = sources.get(value);
if (s !== void 0) {
sources.delete(value);
set(s, false);
}
if (deleted) {
set(__privateGet(this, _size), super.size);
increment(__privateGet(this, _version));
}
return deleted;
}
clear() {
if (super.size === 0) {
return;
}
super.clear();
var sources = __privateGet(this, _sources);
for (var s of sources.values()) {
set(s, false);
}
sources.clear();
set(__privateGet(this, _size), 0);
increment(__privateGet(this, _version));
}
keys() {
return this.values();
}
values() {
get(__privateGet(this, _version));
return super.values();
}
entries() {
get(__privateGet(this, _version));
return super.entries();
}
[Symbol.iterator]() {
return this.keys();
}
get size() {
return get(__privateGet(this, _size));
}
};
_sources = new WeakMap();
_version = new WeakMap();
_size = new WeakMap();
_update_version = new WeakMap();
_SvelteSet_instances = new WeakSet();
/**
* If the source is being created inside the same reaction as the SvelteSet instance,
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
* use `source` so it will be.
*
* @template T
* @param {T} value
* @returns {Source<T>}
*/
source_fn = function(value) {
return update_version === __privateGet(this, _update_version) ? state(value) : source(value);
};
// We init as part of the first instance so that we can treeshake this class
init_fn2 = function() {
inited2 = true;
var proto = _SvelteSet.prototype;
var set_proto = Set.prototype;
for (const method of read_methods) {
proto[method] = function(...v) {
get(__privateGet(this, _version));
return set_proto[method].apply(this, v);
};
}
for (const method of set_like_methods) {
proto[method] = function(...v) {
get(__privateGet(this, _version));
var set2 = (
/** @type {Set<T>} */
set_proto[method].apply(this, v)
);
return new _SvelteSet(set2);
};
}
};
var SvelteSet = _SvelteSet;
// node_modules/svelte/src/reactivity/map.js
var _sources2, _version2, _size2, _update_version2, _SvelteMap_instances, source_fn2, read_all_fn;
var _SvelteMap = class _SvelteMap extends Map {
/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
*/
constructor(value) {
super();
__privateAdd(this, _SvelteMap_instances);
/** @type {Map<K, Source<number>>} */
__privateAdd(this, _sources2, /* @__PURE__ */ new Map());
__privateAdd(this, _version2, state(0));
__privateAdd(this, _size2, state(0));
__privateAdd(this, _update_version2, update_version || -1);
if (true_default) {
value = new Map(value);
tag(__privateGet(this, _version2), "SvelteMap version");
tag(__privateGet(this, _size2), "SvelteMap.size");
}
if (value) {
for (var [key, v] of value) {
super.set(key, v);
}
__privateGet(this, _size2).v = super.size;
}
}
/** @param {K} key */
has(key) {
var sources = __privateGet(this, _sources2);
var s = sources.get(key);
if (s === void 0) {
var ret = super.get(key);
if (ret !== void 0) {
s = __privateMethod(this, _SvelteMap_instances, source_fn2).call(this, 0);
if (true_default) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
} else {
get(__privateGet(this, _version2));
return false;
}
}
get(s);
return true;
}
/**
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
* @param {any} [this_arg]
*/
forEach(callbackfn, this_arg) {
__privateMethod(this, _SvelteMap_instances, read_all_fn).call(this);
super.forEach(callbackfn, this_arg);
}
/** @param {K} key */
get(key) {
var sources = __privateGet(this, _sources2);
var s = sources.get(key);
if (s === void 0) {
var ret = super.get(key);
if (ret !== void 0) {
s = __privateMethod(this, _SvelteMap_instances, source_fn2).call(this, 0);
if (true_default) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
} else {
get(__privateGet(this, _version2));
return void 0;
}
}
get(s);
return super.get(key);
}
/**
* @param {K} key
* @param {V} value
* */
set(key, value) {
var _a;
var sources = __privateGet(this, _sources2);
var s = sources.get(key);
var prev_res = super.get(key);
var res = super.set(key, value);
var version = __privateGet(this, _version2);
if (s === void 0) {
s = __privateMethod(this, _SvelteMap_instances, source_fn2).call(this, 0);
if (true_default) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
set(__privateGet(this, _size2), super.size);
increment(version);
} else if (prev_res !== value) {
increment(s);
var v_reactions = version.reactions === null ? null : new Set(version.reactions);
var needs_version_increase = v_reactions === null || !((_a = s.reactions) == null ? void 0 : _a.every(
(r) => (
/** @type {NonNullable<typeof v_reactions>} */
v_reactions.has(r)
)
));
if (needs_version_increase) {
increment(version);
}
}
return res;
}
/** @param {K} key */
delete(key) {
var sources = __privateGet(this, _sources2);
var s = sources.get(key);
var res = super.delete(key);
if (s !== void 0) {
sources.delete(key);
set(__privateGet(this, _size2), super.size);
set(s, -1);
increment(__privateGet(this, _version2));
}
return res;
}
clear() {
if (super.size === 0) {
return;
}
super.clear();
var sources = __privateGet(this, _sources2);
set(__privateGet(this, _size2), 0);
for (var s of sources.values()) {
set(s, -1);
}
increment(__privateGet(this, _version2));
sources.clear();
}
keys() {
get(__privateGet(this, _version2));
return super.keys();
}
values() {
__privateMethod(this, _SvelteMap_instances, read_all_fn).call(this);
return super.values();
}
entries() {
__privateMethod(this, _SvelteMap_instances, read_all_fn).call(this);
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(__privateGet(this, _size2));
return super.size;
}
};
_sources2 = new WeakMap();
_version2 = new WeakMap();
_size2 = new WeakMap();
_update_version2 = new WeakMap();
_SvelteMap_instances = new WeakSet();
/**
* If the source is being created inside the same reaction as the SvelteMap instance,
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
* use `source` so it will be.
*
* @template T
* @param {T} value
* @returns {Source<T>}
*/
source_fn2 = function(value) {
return update_version === __privateGet(this, _update_version2) ? state(value) : source(value);
};
read_all_fn = function() {
get(__privateGet(this, _version2));
var sources = __privateGet(this, _sources2);
if (__privateGet(this, _size2).v !== sources.size) {
for (var key of __superGet(_SvelteMap.prototype, this, "keys").call(this)) {
if (!sources.has(key)) {
var s = __privateMethod(this, _SvelteMap_instances, source_fn2).call(this, 0);
if (true_default) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
}
}
}
for ([, s] of __privateGet(this, _sources2)) {
get(s);
}
};
var SvelteMap = _SvelteMap;
// node_modules/svelte/src/reactivity/url-search-params.js
var REPLACE = Symbol();
var _version3, _url, _updating, _SvelteURLSearchParams_instances, update_url_fn;
var SvelteURLSearchParams = class extends URLSearchParams {
constructor() {
super(...arguments);
__privateAdd(this, _SvelteURLSearchParams_instances);
__privateAdd(this, _version3, true_default ? tag(state(0), "SvelteURLSearchParams version") : state(0));
__privateAdd(this, _url, get_current_url());
__privateAdd(this, _updating, false);
}
/**
* @param {URLSearchParams} params
* @internal
*/
[REPLACE](params) {
if (__privateGet(this, _updating)) return;
__privateSet(this, _updating, true);
for (const key of [...super.keys()]) {
super.delete(key);
}
for (const [key, value] of params) {
super.append(key, value);
}
increment(__privateGet(this, _version3));
__privateSet(this, _updating, false);
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
append(name, value) {
super.append(name, value);
__privateMethod(this, _SvelteURLSearchParams_instances, update_url_fn).call(this);
increment(__privateGet(this, _version3));
}
/**
* @param {string} name
* @param {string=} value
* @returns {void}
*/
delete(name, value) {
var has_value = super.has(name, value);
super.delete(name, value);
if (has_value) {
__privateMethod(this, _SvelteURLSearchParams_instances, update_url_fn).call(this);
increment(__privateGet(this, _version3));
}
}
/**
* @param {string} name
* @returns {string|null}
*/
get(name) {
get(__privateGet(this, _version3));
return super.get(name);
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
get(__privateGet(this, _version3));
return super.getAll(name);
}
/**
* @param {string} name
* @param {string=} value
* @returns {boolean}
*/
has(name, value) {
get(__privateGet(this, _version3));
return super.has(name, value);
}
keys() {
get(__privateGet(this, _version3));
return super.keys();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
set(name, value) {
var previous = super.getAll(name).join("");
super.set(name, value);
if (previous !== super.getAll(name).join("")) {
__privateMethod(this, _SvelteURLSearchParams_instances, update_url_fn).call(this);
increment(__privateGet(this, _version3));
}
}
sort() {
super.sort();
__privateMethod(this, _SvelteURLSearchParams_instances, update_url_fn).call(this);
increment(__privateGet(this, _version3));
}
toString() {
get(__privateGet(this, _version3));
return super.toString();
}
values() {
get(__privateGet(this, _version3));
return super.values();
}
entries() {
get(__privateGet(this, _version3));
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(__privateGet(this, _version3));
return super.size;
}
};
_version3 = new WeakMap();
_url = new WeakMap();
_updating = new WeakMap();
_SvelteURLSearchParams_instances = new WeakSet();
update_url_fn = function() {
if (!__privateGet(this, _url) || __privateGet(this, _updating)) return;
__privateSet(this, _updating, true);
const search = this.toString();
__privateGet(this, _url).search = search && `?${search}`;
__privateSet(this, _updating, false);
};
// node_modules/svelte/src/reactivity/url.js
var current_url = null;
function get_current_url() {
return current_url;
}
var _protocol, _username, _password, _hostname, _port, _pathname, _hash, _search, _searchParams;
var SvelteURL = class extends URL {
/**
* @param {string | URL} url
* @param {string | URL} [base]
*/
constructor(url, base) {
url = new URL(url, base);
super(url);
__privateAdd(this, _protocol, state(super.protocol));
__privateAdd(this, _username, state(super.username));
__privateAdd(this, _password, state(super.password));
__privateAdd(this, _hostname, state(super.hostname));
__privateAdd(this, _port, state(super.port));
__privateAdd(this, _pathname, state(super.pathname));
__privateAdd(this, _hash, state(super.hash));
__privateAdd(this, _search, state(super.search));
__privateAdd(this, _searchParams);
if (true_default) {
tag(__privateGet(this, _protocol), "SvelteURL.protocol");
tag(__privateGet(this, _username), "SvelteURL.username");
tag(__privateGet(this, _password), "SvelteURL.password");
tag(__privateGet(this, _hostname), "SvelteURL.hostname");
tag(__privateGet(this, _port), "SvelteURL.port");
tag(__privateGet(this, _pathname), "SvelteURL.pathname");
tag(__privateGet(this, _hash), "SvelteURL.hash");
tag(__privateGet(this, _search), "SvelteURL.search");
}
current_url = this;
__privateSet(this, _searchParams, new SvelteURLSearchParams(url.searchParams));
current_url = null;
}
get hash() {
return get(__privateGet(this, _hash));
}
set hash(value) {
super.hash = value;
set(__privateGet(this, _hash), super.hash);
}
get host() {
get(__privateGet(this, _hostname));
get(__privateGet(this, _port));
return super.host;
}
set host(value) {
super.host = value;
set(__privateGet(this, _hostname), super.hostname);
set(__privateGet(this, _port), super.port);
}
get hostname() {
return get(__privateGet(this, _hostname));
}
set hostname(value) {
super.hostname = value;
set(__privateGet(this, _hostname), super.hostname);
}
get href() {
get(__privateGet(this, _protocol));
get(__privateGet(this, _username));
get(__privateGet(this, _password));
get(__privateGet(this, _hostname));
get(__privateGet(this, _port));
get(__privateGet(this, _pathname));
get(__privateGet(this, _hash));
get(__privateGet(this, _search));
return super.href;
}
set href(value) {
super.href = value;
set(__privateGet(this, _protocol), super.protocol);
set(__privateGet(this, _username), super.username);
set(__privateGet(this, _password), super.password);
set(__privateGet(this, _hostname), super.hostname);
set(__privateGet(this, _port), super.port);
set(__privateGet(this, _pathname), super.pathname);
set(__privateGet(this, _hash), super.hash);
set(__privateGet(this, _search), super.search);
__privateGet(this, _searchParams)[REPLACE](super.searchParams);
}
get password() {
return get(__privateGet(this, _password));
}
set password(value) {
super.password = value;
set(__privateGet(this, _password), super.password);
}
get pathname() {
return get(__privateGet(this, _pathname));
}
set pathname(value) {
super.pathname = value;
set(__privateGet(this, _pathname), super.pathname);
}
get port() {
return get(__privateGet(this, _port));
}
set port(value) {
super.port = value;
set(__privateGet(this, _port), super.port);
}
get protocol() {
return get(__privateGet(this, _protocol));
}
set protocol(value) {
super.protocol = value;
set(__privateGet(this, _protocol), super.protocol);
}
get search() {
return get(__privateGet(this, _search));
}
set search(value) {
super.search = value;
set(__privateGet(this, _search), value);
__privateGet(this, _searchParams)[REPLACE](super.searchParams);
}
get username() {
return get(__privateGet(this, _username));
}
set username(value) {
super.username = value;
set(__privateGet(this, _username), super.username);
}
get origin() {
get(__privateGet(this, _protocol));
get(__privateGet(this, _hostname));
get(__privateGet(this, _port));
return super.origin;
}
get searchParams() {
return __privateGet(this, _searchParams);
}
toString() {
return this.href;
}
toJSON() {
return this.href;
}
};
_protocol = new WeakMap();
_username = new WeakMap();
_password = new WeakMap();
_hostname = new WeakMap();
_port = new WeakMap();
_pathname = new WeakMap();
_hash = new WeakMap();
_search = new WeakMap();
_searchParams = new WeakMap();
// node_modules/svelte/src/reactivity/media-query.js
var parenthesis_regex = /\(.+\)/;
var non_parenthesized_keywords = /* @__PURE__ */ new Set(["all", "print", "screen", "and", "or", "not", "only"]);
var MediaQuery = class extends ReactiveValue {
/**
* @param {string} query A media query string
* @param {boolean} [fallback] Fallback value for the server
*/
constructor(query, fallback) {
let final_query = parenthesis_regex.test(query) || // we need to use `some` here because technically this `window.matchMedia('random,screen')` still returns true
query.split(/[\s,]+/).some((keyword) => non_parenthesized_keywords.has(keyword.trim())) ? query : `(${query})`;
const q = window.matchMedia(final_query);
super(
() => q.matches,
(update) => on(q, "change", update)
);
}
};
export {
SvelteDate,
SvelteSet,
SvelteMap,
SvelteURLSearchParams,
SvelteURL,
MediaQuery
};
//# sourceMappingURL=chunk-EZEETTA4.js.map
File diff suppressed because one or more lines are too long
-4841
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-37
View File
@@ -1,37 +0,0 @@
var __defProp = Object.defineProperty;
var __getProtoOf = Object.getPrototypeOf;
var __reflectGet = Reflect.get;
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
var __privateWrapper = (obj, member, setter, getter) => ({
set _(value) {
__privateSet(obj, member, value, setter);
},
get _() {
return __privateGet(obj, member, getter);
}
});
var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
export {
__export,
__publicField,
__privateGet,
__privateAdd,
__privateSet,
__privateMethod,
__privateWrapper,
__superGet
};
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
+1 -1
View File
@@ -2,7 +2,7 @@ import {
clsx,
clsx_default
} from "./chunk-U7P2NEEE.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
clsx,
clsx_default as default
+1 -1
View File
@@ -1,4 +1,4 @@
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/devalue/src/utils.js
var escaped = {
+1 -1
View File
@@ -2,7 +2,7 @@ import {
false_default,
true_default
} from "./chunk-HNWPC2PS.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
true_default as BROWSER,
true_default as DEV,
+1 -1
View File
@@ -1,4 +1,4 @@
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/pocketbase/dist/pocketbase.es.mjs
var ClientResponseError = class _ClientResponseError extends Error {
+5 -5
View File
@@ -7,13 +7,13 @@ import {
hydratable,
onDestroy,
onMount
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
hydrate,
mount,
unmount
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
createContext,
flushSync,
@@ -25,11 +25,11 @@ import {
settled,
tick,
untrack
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
afterUpdate,
beforeUpdate,
+1 -1
View File
@@ -2,7 +2,7 @@ import {
clsx,
clsx_default
} from "./chunk-U7P2NEEE.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
clsx,
clsx_default as default
+1 -1
View File
@@ -1,7 +1,7 @@
import {
cubicOut
} from "./chunk-YERFD2CZ.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/svelte/src/animate/index.js
function flip(node, { from, to }, params = {}) {
+4 -4
View File
@@ -1,14 +1,14 @@
import {
createAttachmentKey,
fromAction
} from "./chunk-EXK7SDUS.js";
} from "./chunk-NTAG77ZX.js";
import "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import "./chunk-57IKBECK.js";
import "./chunk-JC3VXQM7.js";
import "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
createAttachmentKey,
fromAction
+1 -1
View File
@@ -31,7 +31,7 @@ import {
sineInOut,
sineOut
} from "./chunk-YERFD2CZ.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
backIn,
backInOut,
+2 -2
View File
@@ -1,11 +1,11 @@
import "./chunk-7RQDXF5S.js";
import {
on
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
on
};
+5 -5
View File
@@ -107,8 +107,7 @@ import {
validate_store,
validate_void_dynamic_element,
wrap_snippet
} from "./chunk-EXK7SDUS.js";
import "./chunk-U7P2NEEE.js";
} from "./chunk-NTAG77ZX.js";
import {
append,
comment,
@@ -126,7 +125,8 @@ import {
text,
trusted,
with_script
} from "./chunk-57IKBECK.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import {
$document,
$window,
@@ -200,11 +200,11 @@ import {
user_effect,
user_pre_effect,
wait
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
CLASS,
FILENAME,
+1 -1
View File
@@ -1,2 +1,2 @@
import "./chunk-2LGM3QYM.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
+1 -1
View File
@@ -1,7 +1,7 @@
import {
enable_async_mode_flag
} from "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/svelte/src/internal/flags/async.js
enable_async_mode_flag();
+1 -1
View File
@@ -1,3 +1,3 @@
import "./chunk-MZVN5SDE.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
+1 -1
View File
@@ -1,7 +1,7 @@
import {
enable_tracing_mode_flag
} from "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/svelte/src/internal/flags/tracing.js
enable_tracing_mode_flag();
+3 -3
View File
@@ -12,12 +12,12 @@ import {
stopImmediatePropagation,
stopPropagation,
trusted
} from "./chunk-57IKBECK.js";
import "./chunk-JC3VXQM7.js";
} from "./chunk-MKTP2BFS.js";
import "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
asClassComponent,
createBubbler,
+9 -9
View File
@@ -1,18 +1,15 @@
import {
MediaQuery
} from "./chunk-EZEETTA4.js";
import "./chunk-3TACVO2P.js";
} from "./chunk-N4HEU72H.js";
import "./chunk-MPD7DSJG.js";
import "./chunk-7RQDXF5S.js";
import {
linear
} from "./chunk-YERFD2CZ.js";
import {
loop,
raf,
writable
} from "./chunk-EXK7SDUS.js";
} from "./chunk-NTAG77ZX.js";
import "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import "./chunk-57IKBECK.js";
import {
deferred,
get,
@@ -21,18 +18,21 @@ import {
set,
state,
tag
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import {
true_default
} from "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import {
linear
} from "./chunk-YERFD2CZ.js";
import {
__privateAdd,
__privateGet,
__privateMethod,
__privateSet
} from "./chunk-X4VJQ2O3.js";
} from "./chunk-LYA4E26T.js";
// node_modules/svelte/src/motion/utils.js
function is_date(obj) {
+6 -6
View File
@@ -5,19 +5,19 @@ import {
SvelteSet,
SvelteURL,
SvelteURLSearchParams
} from "./chunk-EZEETTA4.js";
import "./chunk-3TACVO2P.js";
} from "./chunk-N4HEU72H.js";
import "./chunk-MPD7DSJG.js";
import "./chunk-7RQDXF5S.js";
import "./chunk-EXK7SDUS.js";
import "./chunk-NTAG77ZX.js";
import "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import "./chunk-57IKBECK.js";
import {
createSubscriber
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
MediaQuery,
SvelteDate,
+5 -5
View File
@@ -1,17 +1,17 @@
import {
ReactiveValue
} from "./chunk-3TACVO2P.js";
} from "./chunk-MPD7DSJG.js";
import "./chunk-7RQDXF5S.js";
import "./chunk-EXK7SDUS.js";
import "./chunk-NTAG77ZX.js";
import "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import "./chunk-57IKBECK.js";
import {
get,
on,
set,
source,
tag
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import {
true_default
@@ -21,7 +21,7 @@ import {
__privateAdd,
__privateGet,
__privateMethod
} from "./chunk-X4VJQ2O3.js";
} from "./chunk-LYA4E26T.js";
// node_modules/svelte/src/reactivity/window/index.js
var scrollX = new ReactiveValue(
+4 -4
View File
@@ -4,9 +4,9 @@ import {
readable,
readonly,
writable
} from "./chunk-EXK7SDUS.js";
} from "./chunk-NTAG77ZX.js";
import "./chunk-MKTP2BFS.js";
import "./chunk-U7P2NEEE.js";
import "./chunk-57IKBECK.js";
import {
active_effect,
active_reaction,
@@ -16,11 +16,11 @@ import {
render_effect,
set_active_effect,
set_active_reaction
} from "./chunk-JC3VXQM7.js";
} from "./chunk-HZ6NBROY.js";
import "./chunk-XWATFG4W.js";
import "./chunk-HNWPC2PS.js";
import "./chunk-OHYQYV5R.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/svelte/src/store/index-client.js
function toStore(get2, set) {
+1 -1
View File
@@ -4,7 +4,7 @@ import {
import {
true_default
} from "./chunk-HNWPC2PS.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/svelte/src/transition/index.js
var linear = (x) => x;
+1 -1
View File
@@ -8,7 +8,7 @@ import {
twMerge,
validators
} from "./chunk-HRPYXN4O.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
export {
createTailwindMerge,
extendTailwindMerge,
+1 -1
View File
@@ -2,7 +2,7 @@ import {
extendTailwindMerge,
twMerge
} from "./chunk-HRPYXN4O.js";
import "./chunk-X4VJQ2O3.js";
import "./chunk-LYA4E26T.js";
// node_modules/tailwind-variants/dist/chunk-LQJYWU4O.js
var SPACE_REGEX = /\s+/g;
+3 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"dev": "vite dev --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -30,6 +30,8 @@
"dependencies": {
"barcode-detector-api-polyfill": "^1.0.20",
"clsx": "^2.1.1",
"heic-convert": "^2.1.0",
"heic2any": "^0.0.4",
"pocketbase": "^0.26.0",
"tailwind-merge": "^3.0.0"
},
+1
View File
@@ -5,6 +5,7 @@
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='4' fill='%231e293b'/><text x='16' y='22' font-size='18' font-family='system-ui' fill='%2394a3b8' text-anchor='middle'>S</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Stackq" />
+47 -9
View File
@@ -1,8 +1,9 @@
import { redirect } from "@sveltejs/kit";
import PocketBase from "pocketbase";
import type { Handle } from "@sveltejs/kit";
import type { Handle, HandleServerError } from "@sveltejs/kit";
import { POCKETBASE_BASE_URL } from "$lib/pocketbase";
const POCKETBASE_URL = "https://pocketbase.ccllc.pro";
const BASE_SSO_URL = process.env.BASE_SSO_URL ?? "https://base.ccllc.pro";
const LOGIN_PATH = "/login";
const LOGOUT_PATH = "/logout";
@@ -15,7 +16,32 @@ function isPublicRoute(pathname: string): boolean {
}
export const handle: Handle = async ({ event, resolve }) => {
event.locals.pb = new PocketBase(POCKETBASE_URL);
// SSO: if opened from Base with ?sso=token, exchange for session and redirect
const ssoParam = event.url.searchParams.get("sso");
if (ssoParam) {
try {
const res = await fetch(
`${BASE_SSO_URL}/api/sso/exchange?token=${encodeURIComponent(ssoParam)}`
);
if (res.ok) {
const auth = await res.json();
const cookieVal = encodeURIComponent(
JSON.stringify({ token: auth.token, model: auth.model })
);
const setCookie = `pb_auth=${cookieVal}; Path=/; SameSite=Lax; HttpOnly; Max-Age=604800`;
return new Response(null, {
status: 302,
headers: { Location: event.url.pathname, "Set-Cookie": setCookie },
});
} else {
console.error("[SSO] Exchange failed:", res.status, await res.text().catch(() => ""));
}
} catch (err) {
console.error("[SSO] Exchange error:", err);
}
}
event.locals.pb = new PocketBase(POCKETBASE_BASE_URL);
event.locals.pb.authStore.loadFromCookie(event.request.headers.get("cookie") || "");
try {
@@ -37,10 +63,22 @@ export const handle: Handle = async ({ event, resolve }) => {
throw redirect(303, loginUrl);
}
const response = await resolve(event);
response.headers.append(
"set-cookie",
event.locals.pb.authStore.exportToCookie({ httpOnly: false, sameSite: "Lax" })
);
return response;
// Set PocketBase auth cookie via SvelteKit so we don't mutate the response (avoids 502)
const { token, record } = event.locals.pb.authStore;
if (token || record) {
event.cookies.set("pb_auth", JSON.stringify({ token: token ?? "", model: record ?? null }), {
path: "/",
sameSite: "lax",
maxAge: 604800,
httpOnly: false,
});
}
return resolve(event);
};
export const handleError: HandleServerError = ({ error, event }) => {
console.error("[handleError]", event.url.pathname, error);
const message = import.meta.env?.DEV ? String(error) : "Something went wrong.";
return { message };
};
-182
View File
@@ -1,182 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Dialog from "$lib/components/ui/dialog";
let {
open = $bindable(false),
onResult,
title = "Scan barcode",
}: {
open?: boolean;
onResult?: (value: string) => void;
title?: string;
} = $props();
let scanError = $state<string | null>(null);
let searchInput = $state("");
let videoEl = $state<HTMLVideoElement | undefined>(undefined);
let stream = $state<MediaStream | null>(null);
let scanRAF = $state(0);
function submitSearch() {
const value = searchInput.trim();
if (!value) return;
stopScan();
open = false;
searchInput = "";
onResult?.(value);
}
async function ensureBarcodeDetector(): Promise<boolean> {
if (typeof globalThis === "undefined" || "BarcodeDetector" in globalThis) return true;
try {
const mod = await import("barcode-detector-api-polyfill");
(globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector =
mod.BarcodeDetector;
return true;
} catch {
return false;
}
}
async function startScan() {
scanError = null;
const hasDetector = await ensureBarcodeDetector();
if (!hasDetector) {
scanError = "Failed to load barcode scanner. Try again or use Chrome/Edge.";
return;
}
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 } },
});
} catch (e) {
scanError = e instanceof Error ? e.message : "Could not access camera.";
}
}
function stopScan() {
if (scanRAF) {
cancelAnimationFrame(scanRAF);
scanRAF = 0;
}
if (stream) {
stream.getTracks().forEach((t) => t.stop());
stream = null;
}
if (videoEl?.srcObject) {
videoEl.srcObject = null;
}
scanError = null;
}
$effect(() => {
if (!open) {
stopScan();
searchInput = "";
return;
}
startScan();
return () => stopScan();
});
$effect(() => {
if (!open || !stream || !videoEl) return;
videoEl.srcObject = stream;
videoEl.play().catch(() => {});
return () => {
if (videoEl?.srcObject) videoEl.srcObject = null;
};
});
const SCAN_INTERVAL_MS = 300;
function scanLoop() {
if (!videoEl || !("BarcodeDetector" in globalThis)) return;
const detector = new (globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector();
let lastScan = 0;
async function detect() {
if (!open || !videoEl) return;
if (videoEl.readyState < 2) {
scanRAF = requestAnimationFrame(detect);
return;
}
const now = Date.now();
if (now - lastScan >= SCAN_INTERVAL_MS) {
lastScan = now;
try {
const barcodes = await detector.detect(videoEl);
if (barcodes.length > 0) {
const value = barcodes[0].rawValue?.trim() ?? "";
stopScan();
open = false;
onResult?.(value);
return;
}
} catch {
// ignore
}
}
scanRAF = requestAnimationFrame(detect);
}
detect();
}
$effect(() => {
if (!open || !stream || !videoEl) return;
scanLoop();
return () => {
if (scanRAF) cancelAnimationFrame(scanRAF);
};
});
</script>
<Dialog.Root bind:open>
<Dialog.Content
class="flex max-h-[100dvh] w-[100vw] max-w-none flex-col gap-4 rounded-none border-0 p-4 sm:max-h-[90dvh] sm:w-auto sm:max-w-md sm:rounded-lg sm:border"
onclose={() => stopScan()}
>
<Dialog.Header class="shrink-0">
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>Scan a barcode or type a code to look up an item.</Dialog.Description>
</Dialog.Header>
<div class="flex min-h-0 flex-1 flex-col gap-4">
<div class="flex shrink-0 gap-2">
<Input
type="text"
placeholder="Type barcode or SKU to search…"
bind:value={searchInput}
onkeydown={(e) => e.key === "Enter" && submitSearch()}
class="min-h-11 flex-1"
/>
<Button type="button" class="min-h-11 shrink-0" onclick={submitSearch} disabled={!searchInput.trim()}>
Search
</Button>
</div>
<p class="text-muted-foreground shrink-0 text-center text-xs">or use camera below</p>
{#if scanError}
<p class="text-destructive text-sm">{scanError}</p>
{:else if stream}
<video
bind:this={videoEl}
class="border-input max-h-[50vh] w-full flex-1 rounded-lg border object-cover sm:max-h-none"
muted
playsinline
></video>
{:else}
<p class="text-muted-foreground text-sm">Starting camera…</p>
{/if}
<div class="flex shrink-0 gap-2">
<Button
type="button"
variant="outline"
class="min-h-12 flex-1"
onclick={() => { stopScan(); open = false; }}
>
Cancel
</Button>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
@@ -0,0 +1,21 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CollapsiblePrimitive.ContentProps & HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
</script>
<CollapsiblePrimitive.Content
bind:ref
class={cn("overflow-hidden", className)}
{...restProps}
>
{@render children?.()}
</CollapsiblePrimitive.Content>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CollapsiblePrimitive.TriggerProps & HTMLButtonAttributes<HTMLButtonElement> & { children?: Snippet } = $props();
</script>
<CollapsiblePrimitive.Trigger bind:ref class={cn("flex items-center gap-2", className)} {...restProps}>
{@render children?.()}
</CollapsiblePrimitive.Trigger>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import type { Snippet } from "svelte";
import { cn } from "$lib/utils.js";
let {
open = $bindable(false),
class: className,
children,
...restProps
}: CollapsiblePrimitive.RootProps & HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
</script>
<CollapsiblePrimitive.Root bind:open class={cn(className)} {...restProps}>
{@render children?.()}
</CollapsiblePrimitive.Root>
@@ -0,0 +1,5 @@
import Root from "./collapsible.svelte";
import Content from "./collapsible-content.svelte";
import Trigger from "./collapsible-trigger.svelte";
export { Root, Content, Trigger, Root as Collapsible, Content as CollapsibleContent, Trigger as CollapsibleTrigger };
-34
View File
@@ -1,34 +0,0 @@
/** UOM options for Stackq_Items (single select). */
export const UOM_OPTIONS = ["EA", "BX", "CA", "PK", "PLT", "LF"] as const;
/** Catagory options (single select in schema). */
export const CATAGORY_OPTIONS = [
"Framing",
"Insulation",
"Drywall",
"T-Bar",
"Painting",
"Masking",
"Misc",
] as const;
/** Sub_Catagory options (single select in schema). */
export const SUB_CATAGORY_OPTIONS = [
"ACT",
"Channel",
"Fasteners",
"Finishing Materials",
"Adhesives",
"Mud",
"Corner Bead",
"Sanding",
"Tape",
"Metal Studs",
"Metal Tracks",
"FRP",
"Gyp",
"Insulation",
"Paint",
"Weatherproofing",
"Misc",
] as const;
+39
View File
@@ -0,0 +1,39 @@
/**
* Server-side HEIC conversion using heic-convert (Node). Use in form actions only.
*/
export function isHeicFile(file: File): boolean {
const name = file.name.toLowerCase();
const type = (file.type ?? "").toLowerCase();
return (
name.endsWith(".heic") ||
name.endsWith(".heif") ||
type === "image/heic" ||
type === "image/heif"
);
}
/**
* If the file is HEIC/HEIF, convert to JPEG and return a new File. Otherwise return the original.
*/
export async function convertHeicFileToJpegIfNeeded(file: File): Promise<File> {
if (!isHeicFile(file)) return file;
const arrayBuffer = await file.arrayBuffer();
const inputBuffer = Buffer.from(arrayBuffer);
const convert = (await import("heic-convert")).default as (opts: {
buffer: Buffer;
format: string;
quality?: number;
}) => Promise<Buffer>;
const outputBuffer = await convert({
buffer: inputBuffer,
format: "JPEG",
quality: 0.92,
});
const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
return new File([outputBuffer], `${baseName}.jpg`, {
type: "image/jpeg",
lastModified: Date.now(),
});
}
+34
View File
@@ -0,0 +1,34 @@
function isHeifConsoleMessage(msg: string): boolean {
return /HEIF|parse HEIF|Could not parse/i.test(msg);
}
/**
* Convert HEIC/HEIF blob to JPEG. Uses heic2any only when this function is called
* (dynamic import), so the receipt page can load even if heic2any fails to resolve.
* Suppresses the library's "Could not parse HEIF file" console output during conversion.
*/
export async function convertHeicBlobToJpeg(blob: Blob): Promise<Blob> {
const originalError = console.error;
const originalWarn = console.warn;
console.error = (...args: unknown[]) => {
const msg = args[0]?.toString?.() ?? "";
if (isHeifConsoleMessage(msg)) return;
originalError.apply(console, args);
};
console.warn = (...args: unknown[]) => {
const msg = args[0]?.toString?.() ?? "";
if (isHeifConsoleMessage(msg)) return;
originalWarn.apply(console, args);
};
try {
const mod = await import(/* @vite-ignore */ "heic2any");
const heic2any = (mod as unknown as {
default: (opts: { blob: Blob; toType: string; quality?: number }) => Promise<Blob | Blob[]>;
}).default;
const out = await heic2any({ blob, toType: "image/jpeg", quality: 0.92 });
return Array.isArray(out) ? out[0] : out;
} finally {
console.error = originalError;
console.warn = originalWarn;
}
}
+117
View File
@@ -0,0 +1,117 @@
import { convertHeicBlobToJpeg } from "./convert-heic";
export function isHeicFile(file: File): boolean {
const name = file.name.toLowerCase();
const type = (file.type ?? "").toLowerCase();
return (
name.endsWith(".heic") ||
name.endsWith(".heif") ||
type === "image/heic" ||
type === "image/heif"
);
}
/** True if the file is an image we can process (image/* or HEIC by type/extension). */
export function isAcceptableImageFile(file: File | null): boolean {
if (!file) return false;
const type = (file.type ?? "").toLowerCase();
return type.startsWith("image/") || isHeicFile(file);
}
function makeOutputFile(blob: Blob, baseName: string, ext: string): File {
return new File([blob], `${baseName}.${ext}`, {
type: blob.type,
lastModified: Date.now(),
});
}
function isSafari(): boolean {
if (typeof navigator === "undefined") return false;
const ua = navigator.userAgent;
return ua.includes("Safari") && !ua.includes("Chrome");
}
/** Load blob in Image, draw to canvas, encode as WebP or JPEG. Rejects if image fails to load or encode. */
function tryCanvas(sourceBlob: Blob, baseName: string, quality: number): Promise<File> {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(sourceBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Could not get canvas context"));
return;
}
ctx.drawImage(img, 0, 0);
// Safari doesn't support WebP in toBlob; try WebP first, fall back to JPEG
canvas.toBlob(
(blob) => {
if (blob && blob.type === "image/webp") {
resolve(makeOutputFile(blob, baseName, "webp"));
return;
}
canvas.toBlob(
(jpegBlob) => {
if (!jpegBlob) {
reject(new Error("Failed to encode image"));
return;
}
resolve(makeOutputFile(jpegBlob, baseName, "jpg"));
},
"image/jpeg",
quality
);
},
"image/webp",
quality
);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("Failed to load image"));
};
img.src = url;
});
}
/**
* Converts an image File to WebP (or JPEG on Safari where WebP isn't supported).
* HEIC: Safari uses native Image() first, then heic2any fallback; other browsers use heic2any first, then Image() fallback.
*/
export async function convertImageToWebP(file: File, quality = 0.85): Promise<File> {
const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
if (isHeicFile(file)) {
const safari = isSafari();
if (safari) {
// Safari can decode HEIC in Image(); heic2any is unreliable on Safari (e.g. transparent output)
try {
return await tryCanvas(file, baseName, quality);
} catch {
try {
const jpegBlob = await convertHeicBlobToJpeg(file);
return await tryCanvas(jpegBlob, baseName, quality);
} catch (e) {
throw e instanceof Error ? e : new Error("Failed to convert HEIC");
}
}
}
// Non-Safari: Image() usually can't decode HEIC; use heic2any first
try {
const jpegBlob = await convertHeicBlobToJpeg(file);
return await tryCanvas(jpegBlob, baseName, quality);
} catch {
try {
return await tryCanvas(file, baseName, quality);
} catch (e) {
throw e instanceof Error ? e : new Error("Failed to convert HEIC");
}
}
}
return await tryCanvas(file, baseName, quality);
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Parse form values. Used by receipts server actions.
*/
export function parseNum(val: unknown): number | undefined {
if (val === "" || val === null || val === undefined) return undefined;
const n = Number(val);
return Number.isFinite(n) ? n : undefined;
}
/** Like parseNum but return a number (default when empty/invalid). */
export function parseNumWithDefault(val: unknown, defaultVal: number): number {
const n = parseNum(val);
return n ?? defaultVal;
}
export function parseBool(val: unknown): boolean {
return val === "on" || val === "true" || val === true;
}
-21
View File
@@ -1,21 +0,0 @@
/** PocketBase Stackq_Items collection record (and expanded relation). */
export interface StackqItem {
id: string;
Item: string;
SKU: string;
Description: string;
UOM: string;
Cost: number;
Vendor: string;
Catagory: string | string[];
Sub_Catagory: string | string[];
Has_Parent: boolean;
Parent: string; // relation id
Stock: boolean;
Image?: string; // file filename
created: string;
updated: string;
expand?: {
Parent?: { id: string; Item?: string; SKU?: string };
};
}
+35
View File
@@ -0,0 +1,35 @@
/** Receipt status values for Stackq_Receipts */
export const STATUS_OPTIONS = ["New", "Data Entered", "Reviewed", "Consolidated"] as const;
export type ReceiptStatus = (typeof STATUS_OPTIONS)[number];
/** Line item stored in receipt Data.lineItems */
export interface ReceiptLineItem {
item: string;
description: string;
quantity: number;
price: number;
}
/** JSON shape stored in Stackq_Receipts.Data */
export interface ReceiptDataShape {
lineItems?: ReceiptLineItem[];
tax?: number;
/** Total printed on receipt (for validation) */
receiptTotal?: number;
}
/** PocketBase Stackq_Receipts collection record (and expanded relation). */
export interface StackqReceipt {
id: string;
Status: string;
Type: string;
Description?: string;
User: string;
Image?: string;
Data?: ReceiptDataShape | Record<string, unknown>;
created: string;
updated: string;
expand?: {
User?: { id: string; email?: string; name?: string };
};
}
+33
View File
@@ -0,0 +1,33 @@
/** Allowed MIME types for receipt/item image uploads (server-side check). */
const ALLOWED_IMAGE_TYPES = new Set([
"image/jpeg",
"image/png",
"image/webp",
"image/heic",
"image/heif",
]);
/** Max size 10MB. */
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
/**
* Validates an image file for upload. Returns an error message if invalid, or null if valid/no file.
* Use in form actions before sending the file to PocketBase.
*/
export function validateImageUpload(file: File | null): string | null {
if (!file || file.size === 0) return null;
const type = (file.type ?? "").toLowerCase();
const name = file.name.toLowerCase();
const isHeic =
type === "image/heic" ||
type === "image/heif" ||
name.endsWith(".heic") ||
name.endsWith(".heif");
if (!isHeic && !ALLOWED_IMAGE_TYPES.has(type)) {
return "Invalid image type. Use JPEG, PNG, WebP, or HEIC.";
}
if (file.size > MAX_IMAGE_BYTES) {
return `Image must be under ${MAX_IMAGE_BYTES / 1024 / 1024}MB.`;
}
return null;
}
+3 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { enhance } from '$app/forms';
import { invalidate } from '$app/navigation';
import { browser } from '$app/environment';
@@ -11,6 +12,7 @@
let { data, children }: { data: { user: { id: string; email?: string; name?: string; darkmode: boolean } | null }; children?: import('svelte').Snippet } = $props();
const isReceiptDetailFull = $derived(!!$page?.url?.pathname?.match(/^\/receipts\/[^/]+$/));
let darkmode = $state(false);
$effect(() => {
@@ -38,7 +40,7 @@
}
</script>
<div class="app-shell">
<div class="app-shell" class:receipt-detail-full={isReceiptDetailFull}>
{#if data.user}
<header
class="fixed right-0 top-0 z-50 flex items-center gap-2 p-2 pr-[max(0.5rem,env(safe-area-inset-right))] pt-[max(0.5rem,env(safe-area-inset-top))]"
+2 -11
View File
@@ -1,16 +1,7 @@
import type { Actions, PageServerLoad } from "./$types";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
export const load: PageServerLoad = async ({ locals }) => {
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqItem>({ sort: "-created", expand: "Parent" })
.catch(() => [] as StackqItem[]);
const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU }));
return { items: list, itemsForParent };
export const load: PageServerLoad = async () => {
return {};
};
export const actions: Actions = {
+34 -38
View File
@@ -1,43 +1,39 @@
<script lang="ts">
import { browser } from "$app/environment";
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import ReceiptForm from "./receipts/ReceiptForm.svelte";
import { goto } from "$app/navigation";
let { data }: { data: { items: { id: string; SKU?: string }[]; itemsForParent: { id: string; Item: string; SKU: string }[] } } = $props();
let receiptDialogOpen = $state(false);
function onReceiptSuccess() {
receiptDialogOpen = false;
goto("/receipts");
}
</script>
{#if browser}
{#await import("./HomeWithDialog.svelte")}
<main class="flex min-h-0 flex-1 flex-col items-center justify-center gap-6 p-6">
<p class="text-muted-foreground">Loading…</p>
</main>
{:then { default: HomeWithDialog }}
<HomeWithDialog data={data} />
{/await}
{:else}
<main class="flex min-h-0 flex-1 flex-col items-center justify-center gap-6 p-6">
<h1 class="text-2xl font-semibold">Stackq</h1>
<div class="flex w-full max-w-xs flex-col gap-3">
<a href="/items" class="block">
<Button class="h-12 w-full min-h-12">Scan</Button>
</a>
<a href="/transactions" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">New Transaction</Button>
</a>
<a href="/items" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">New Item</Button>
</a>
</div>
<div class="text-muted-foreground w-full max-w-xs border-t pt-4 text-center text-sm">Go to</div>
<div class="flex w-full max-w-xs flex-col gap-2">
<a href="/items" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">Items</Button>
</a>
<a href="/transactions" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">Transactions</Button>
</a>
<a href="/accounts" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">Accounts</Button>
</a>
</div>
</main>
{/if}
<main class="flex min-h-0 flex-1 flex-col items-center justify-center gap-6 p-6">
<h1 class="text-2xl font-semibold">Stackq</h1>
<div class="flex w-full max-w-xs flex-col gap-3">
<Button class="h-12 min-h-12 w-full" variant="outline" onclick={() => (receiptDialogOpen = true)}>
New receipt
</Button>
<a href="/receipts" class="block">
<Button class="h-12 min-h-12 w-full" variant="outline">Receipts</Button>
</a>
</div>
</main>
<Dialog.Root bind:open={receiptDialogOpen}>
<Dialog.Content class="flex min-h-0 max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[500px] flex-col overflow-hidden rounded-lg sm:w-full">
<Dialog.Header class="shrink-0">
<Dialog.Title>New receipt</Dialog.Title>
<Dialog.Description>Scan or attach a receipt image. You can add details later.</Dialog.Description>
</Dialog.Header>
<ReceiptForm
action="/receipts?/create"
submitLabel="Create"
onSuccess={onReceiptSuccess}
/>
</Dialog.Content>
</Dialog.Root>
-81
View File
@@ -1,81 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import ItemForm from "./items/ItemForm.svelte";
import BarcodeScanDialog from "$lib/components/BarcodeScanDialog.svelte";
import { goto } from "$app/navigation";
let {
data,
}: {
data: {
items: { id: string; SKU?: string }[];
itemsForParent: { id: string; Item: string; SKU: string }[];
};
} = $props();
let dialogOpen = $state(false);
let scanDialogOpen = $state(false);
const items = $derived(data?.items ?? []);
const itemsForParent = $derived(data?.itemsForParent ?? []);
function onItemSuccess() {
dialogOpen = false;
goto("/items");
}
function onScanResult(value: string) {
const sku = value.trim();
const found = items.find((i) => (i.SKU ?? "").trim() === sku);
if (found) goto("/items/" + found.id);
else goto("/items?q=" + encodeURIComponent(sku));
}
</script>
<main class="flex min-h-0 flex-1 flex-col items-center justify-center gap-4 p-4 sm:gap-6 sm:p-6">
<h1 class="w-full max-w-xs shrink-0 text-center text-2xl font-semibold">Stackq</h1>
<div class="flex w-full max-w-xs shrink-0 flex-col gap-3">
<Button class="h-16 min-h-16 w-full" onclick={() => (scanDialogOpen = true)}>Scan</Button>
<a href="/transactions/new" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">New transaction</Button>
</a>
<a href="/receipts/new" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">New receipt</Button>
</a>
<Button class="h-16 min-h-16 w-full" variant="outline" onclick={() => (dialogOpen = true)}>New Item</Button>
</div>
<div class="w-full max-w-xs shrink-0 border-t pt-4"></div>
<div class="flex w-full max-w-xs shrink-0 flex-col gap-2">
<a href="/items" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">Items</Button>
</a>
<a href="/transactions" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">Transactions</Button>
</a>
<a href="/receipts" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">Receipts</Button>
</a>
<a href="/accounts" class="block">
<Button class="h-16 min-h-16 w-full" variant="outline">Accounts</Button>
</a>
</div>
</main>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[500px] overflow-y-auto rounded-lg sm:w-full">
<Dialog.Header>
<Dialog.Title>New item</Dialog.Title>
<Dialog.Description>Add a new Stackq item.</Dialog.Description>
</Dialog.Header>
<ItemForm
itemsForParent={itemsForParent}
action="/items?/create"
submitLabel="Create"
onSuccess={onItemSuccess}
/>
</Dialog.Content>
</Dialog.Root>
<BarcodeScanDialog bind:open={scanDialogOpen} onResult={onScanResult} title="Scan to look up item" />
-5
View File
@@ -1,5 +0,0 @@
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async () => {
return { accounts: [] as { id: string }[] };
};
-75
View File
@@ -1,75 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Card, CardContent } from "$lib/components/ui/card";
import { Input } from "$lib/components/ui/input";
let { data }: { data: { accounts: { id: string }[] } } = $props();
let searchQuery = $state("");
const accounts = $derived(data?.accounts ?? []);
const filteredAccounts = $derived(
(() => {
const q = searchQuery.trim().toLowerCase();
if (!q) return accounts;
return accounts.filter((a) => a.id.toLowerCase().includes(q));
})()
);
</script>
<svelte:head>
<title>Accounts Stackq</title>
</svelte:head>
<main class="container mx-auto flex flex-col gap-6 px-4 py-6 sm:px-6 sm:py-8">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3 min-w-0">
<a href="/" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to main">Back</a>
<h1 class="text-xl font-semibold truncate sm:text-2xl">Accounts</h1>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
<div class="w-full sm:w-64 sm:min-w-0">
<Input
type="search"
placeholder="Search accounts…"
bind:value={searchQuery}
class="h-11 w-full sm:h-9"
/>
</div>
<span class="text-muted-foreground text-sm shrink-0">
{filteredAccounts.length}{filteredAccounts.length !== accounts.length ? ` of ${accounts.length}` : ""} accounts
</span>
<a href="/accounts/new" class="block shrink-0">
<Button class="w-full sm:w-auto min-h-11">New account</Button>
</a>
</div>
</div>
{#if filteredAccounts.length === 0}
<Card>
<CardContent class="flex flex-col items-center justify-center gap-4 py-12">
<p class="text-muted-foreground text-center text-sm">
{accounts.length === 0 ? "No accounts yet." : "No accounts match your search."}
</p>
<p class="text-muted-foreground text-center text-xs">
{accounts.length === 0 ? "Create an account to get started." : "Try a different search."}
</p>
<a href="/accounts/new">
<Button variant="outline">New account</Button>
</a>
<a href="/">
<Button variant="ghost">Back to home</Button>
</a>
</CardContent>
</Card>
{:else}
<div class="flex flex-col gap-2">
{#each filteredAccounts as account (account.id)}
<Card>
<CardContent class="p-4">
<a href="/accounts/{account.id}" class="font-medium hover:underline">Account {account.id}</a>
</CardContent>
</Card>
{/each}
</div>
{/if}
</main>
-26
View File
@@ -1,26 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Card, CardContent } from "$lib/components/ui/card";
let { params }: { params: { id: string } } = $props();
</script>
<svelte:head>
<title>Account Stackq</title>
</svelte:head>
<main class="container mx-auto flex flex-col gap-6 px-4 py-6 sm:px-6 sm:py-8">
<div class="flex items-center gap-3">
<a href="/accounts" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to accounts">Back</a>
<h1 class="text-xl font-semibold sm:text-2xl">Account</h1>
</div>
<Card>
<CardContent class="flex flex-col gap-4 py-8">
<p class="text-muted-foreground text-sm">Account {params.id}</p>
<a href="/accounts">
<Button variant="outline">Back to accounts</Button>
</a>
</CardContent>
</Card>
</main>
-29
View File
@@ -1,29 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Card, CardContent } from "$lib/components/ui/card";
</script>
<svelte:head>
<title>New account Stackq</title>
</svelte:head>
<main class="container mx-auto flex flex-col gap-6 px-4 py-6 sm:px-6 sm:py-8">
<div class="flex items-center gap-3">
<a href="/accounts" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to accounts">Back</a>
<h1 class="text-xl font-semibold sm:text-2xl">New account</h1>
</div>
<Card>
<CardContent class="flex flex-col gap-4 py-8">
<p class="text-muted-foreground text-center text-sm">Account creation will be available here.</p>
<div class="flex justify-center gap-2">
<a href="/accounts">
<Button variant="outline">Back to accounts</Button>
</a>
<a href="/">
<Button variant="ghost">Home</Button>
</a>
</div>
</CardContent>
</Card>
</main>
+69
View File
@@ -0,0 +1,69 @@
import { POCKETBASE_BASE_URL } from "$lib/pocketbase";
import type { RequestHandler } from "./$types";
/** Reject path traversal, leading slash, null bytes, and unreasonably long paths. */
function isSafeFilePath(path: string): boolean {
if (path.length === 0 || path.length > 512) return false;
if (path.includes("..") || path.startsWith("/") || path.includes("\0")) return false;
return true;
}
function isHeicPath(path: string): boolean {
const lower = path.toLowerCase();
return lower.endsWith(".heic") || lower.endsWith(".heif");
}
export const GET: RequestHandler = async ({ params, request }) => {
const path = params.path;
if (!path || !isSafeFilePath(path)) return new Response(null, { status: 404 });
const url = `${POCKETBASE_BASE_URL}/api/files/${path}`;
const cookie = request.headers.get("cookie") ?? "";
const res = await fetch(url, {
headers: { cookie },
});
if (!res.ok) {
return new Response(null, { status: res.status });
}
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
const body = res.body;
if (!body) return new Response(null, { status: 502 });
if (isHeicPath(path)) {
try {
const arrayBuffer = await res.arrayBuffer();
const inputBuffer = Buffer.from(arrayBuffer);
const convert = (await import("heic-convert")).default as (opts: {
buffer: Buffer;
format: string;
quality?: number;
}) => Promise<Buffer>;
const outputBuffer = await convert({
buffer: inputBuffer,
format: "JPEG",
quality: 0.92,
});
return new Response(outputBuffer, {
status: 200,
headers: {
"content-type": "image/jpeg",
"cache-control": res.headers.get("cache-control") ?? "private, max-age=3600",
},
});
} catch (err) {
console.error("[api/files] HEIC conversion failed:", path, err);
return new Response(null, { status: 500 });
}
}
return new Response(body, {
status: 200,
headers: {
"content-type": contentType,
"cache-control": res.headers.get("cache-control") ?? "private, max-age=3600",
},
});
};
-132
View File
@@ -1,132 +0,0 @@
import { fail } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
function parseNum(val: unknown): number | undefined {
if (val === "" || val === null || val === undefined) return undefined;
const n = Number(val);
return Number.isFinite(n) ? n : undefined;
}
function parseBool(val: unknown): boolean {
return val === "on" || val === "true" || val === true;
}
export const load: PageServerLoad = async ({ locals }) => {
const list = await locals.pb
.collection(COLLECTION)
.getFullList<StackqItem>({
sort: "-created",
expand: "Parent",
})
.catch((err) => {
console.error(COLLECTION, err);
return [] as StackqItem[];
});
const itemsForParent = list.map((i) => ({ id: i.id, Item: i.Item, SKU: i.SKU }));
return { items: list, itemsForParent };
};
export const actions: Actions = {
create: async ({ request, locals }) => {
const form = await request.formData();
const Item = (form.get("Item") as string)?.trim() ?? "";
const SKU = (form.get("SKU") as string)?.trim() ?? "";
const Description = (form.get("Description") as string)?.trim() ?? "";
const UOM = (form.get("UOM") as string)?.trim() ?? "";
const Cost = parseNum(form.get("Cost"));
const Vendor = (form.get("Vendor") as string)?.trim() ?? "";
const Catagory = (form.get("Catagory") as string)?.trim() ?? "";
const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? "";
const Has_Parent = parseBool(form.get("Has_Parent"));
const Parent = (form.get("Parent") as string)?.trim() ?? "";
const Stock = parseBool(form.get("Stock"));
const imageFileCreate = form.get("Image") as File | null;
const hasImage = imageFileCreate && imageFileCreate.size > 0;
if (!Item) return fail(400, { error: "Item name is required", form: "create" });
try {
const body: Record<string, unknown> = {
Item,
SKU,
Description,
UOM: UOM || undefined,
Cost: Cost ?? 0,
Vendor,
Catagory: Catagory || undefined,
Sub_Catagory: Sub_Catagory || undefined,
Has_Parent: !!Has_Parent,
Parent: Parent || null,
Stock: !!Stock,
};
if (hasImage) body.Image = imageFileCreate;
await locals.pb.collection(COLLECTION).create(body);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Create failed";
return fail(500, { error: message, form: "create" });
}
return { success: true };
},
update: async ({ request, locals }) => {
const form = await request.formData();
const id = (form.get("id") as string)?.trim();
if (!id) return fail(400, { error: "Missing id", form: "update" });
const Item = (form.get("Item") as string)?.trim() ?? "";
const SKU = (form.get("SKU") as string)?.trim() ?? "";
const Description = (form.get("Description") as string)?.trim() ?? "";
const UOM = (form.get("UOM") as string)?.trim() ?? "";
const Cost = parseNum(form.get("Cost"));
const Vendor = (form.get("Vendor") as string)?.trim() ?? "";
const Catagory = (form.get("Catagory") as string)?.trim() ?? "";
const Sub_Catagory = (form.get("Sub_Catagory") as string)?.trim() ?? "";
const Has_Parent = parseBool(form.get("Has_Parent"));
const Parent = (form.get("Parent") as string)?.trim() ?? "";
const Stock = parseBool(form.get("Stock"));
const Image = form.get("Image") as File | null;
const imageFile = Image && Image.size > 0 ? Image : undefined;
if (!Item) return fail(400, { error: "Item name is required", form: "update" });
try {
const body: Record<string, unknown> = {
Item,
SKU,
Description,
UOM: UOM || undefined,
Cost: Cost ?? 0,
Vendor,
Catagory: Catagory || undefined,
Sub_Catagory: Sub_Catagory || undefined,
Has_Parent: !!Has_Parent,
Parent: Parent || null,
Stock: !!Stock,
};
if (imageFile) body.Image = imageFile;
await locals.pb.collection(COLLECTION).update(id, body);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Update failed";
return fail(500, { error: message, form: "update" });
}
return { success: true };
},
delete: async ({ request, locals }) => {
const form = await request.formData();
const id = (form.get("id") as string)?.trim();
if (!id) return fail(400, { error: "Missing id", form: "delete" });
try {
await locals.pb.collection(COLLECTION).delete(id);
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Delete failed";
return fail(500, { error: message, form: "delete" });
}
return { success: true };
},
};
-92
View File
@@ -1,92 +0,0 @@
<script lang="ts">
import { browser } from "$app/environment";
import { page } from "$app/stores";
import { Badge } from "$lib/components/ui/badge";
import { Card, CardContent } from "$lib/components/ui/card";
import type { StackqItem } from "$lib/types/items";
let { data }: { data: { items: StackqItem[]; itemsForParent: { id: string; Item: string; SKU: string }[] } } = $props();
const searchFromUrl = $derived($page.url.searchParams.get("q") ?? "");
function asList(val: string | string[] | undefined): string[] {
if (val == null) return [];
return Array.isArray(val) ? val : [val];
}
function formatDate(iso: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString(undefined, {
dateStyle: "short",
timeStyle: "short",
});
} catch {
return iso;
}
}
const items = $derived(data?.items ?? []);
const PB_BASE = "https://pocketbase.ccllc.pro";
const COLLECTION = "Stackq_Items";
function imageUrl(item: StackqItem): string | null {
if (!item.Image) return null;
return `${PB_BASE}/api/files/${COLLECTION}/${item.id}/${item.Image}`;
}
</script>
<svelte:head>
<title>Items Stackq</title>
</svelte:head>
{#if browser}
{#await import("./ItemsCrud.svelte")}
<main class="container mx-auto flex flex-col gap-6 px-4 py-8">
<p class="text-muted-foreground">Loading…</p>
</main>
{:then { default: ItemsCrud }}
<ItemsCrud data={data} searchFromUrl={searchFromUrl} />
{/await}
{:else}
<main class="container mx-auto flex flex-col gap-6 px-4 py-8">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-semibold">Stackq Items</h1>
<span class="text-muted-foreground text-sm">{items.length} items</span>
</div>
{#if items.length === 0}
<Card>
<CardContent class="flex h-32 items-center justify-center">
<p class="text-muted-foreground text-sm">No items.</p>
</CardContent>
</Card>
{:else}
<div class="flex flex-col gap-2">
{#each items as item (item.id)}
<Card class="overflow-hidden">
<CardContent class="flex flex-row items-center gap-3 p-3">
<div class="size-12 shrink-0 overflow-hidden rounded border">
{#if imageUrl(item)}
<img src={imageUrl(item)!} alt="" class="size-full object-cover" loading="lazy" />
{:else}
<span class="text-muted-foreground flex size-full items-center justify-center text-xs"></span>
{/if}
</div>
<div class="min-w-0 flex-1">
<a href="/items/{item.id}" class="font-medium hover:underline">{item.Item ?? "—"}</a>
<span class="text-muted-foreground font-mono text-xs"> · {item.SKU ?? "—"}</span>
{#if item.Description}
<span class="text-muted-foreground ml-1 truncate text-xs">· {item.Description}</span>
{/if}
</div>
<div class="flex shrink-0 items-center gap-2 text-xs">
<Badge variant="outline">{item.UOM ?? "—"}</Badge>
<span class="tabular-nums">{item.Cost != null ? Number(item.Cost).toLocaleString() : "—"}</span>
{#if item.Stock}<Badge variant="default" class="text-xs">Stock</Badge>{/if}
</div>
</CardContent>
</Card>
{/each}
</div>
{/if}
</main>
{/if}
-322
View File
@@ -1,322 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { enhance } from "$app/forms";
import { POCKETBASE_BASE_URL } from "$lib/pocketbase";
import { UOM_OPTIONS, CATAGORY_OPTIONS, SUB_CATAGORY_OPTIONS } from "$lib/constants/items";
import type { StackqItem } from "$lib/types/items";
const COLLECTION = "Stackq_Items";
function itemImageUrl(recordId: string, filename: string): string {
return `${POCKETBASE_BASE_URL}/api/files/${COLLECTION}/${recordId}/${filename}`;
}
let {
item = null,
itemsForParent = [],
action,
submitLabel,
error,
onSuccess,
}: {
item?: StackqItem | null;
itemsForParent?: { id: string; Item: string; SKU: string }[];
action: string;
submitLabel: string;
error?: string | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const parentOptions = $derived(
item ? itemsForParent.filter((p) => p.id !== item!.id) : itemsForParent
);
const defaultItem = $derived(item?.Item ?? "");
const defaultSku = $derived(item?.SKU ?? "");
const defaultDesc = $derived(item?.Description ?? "");
const defaultUom = $derived(item?.UOM ?? "");
const defaultCost = $derived(item?.Cost != null ? String(item.Cost) : "");
const defaultVendor = $derived(item?.Vendor ?? "");
const defaultCat = $derived(Array.isArray(item?.Catagory) ? item!.Catagory[0] : item?.Catagory ?? "");
const defaultSub = $derived(Array.isArray(item?.Sub_Catagory) ? item!.Sub_Catagory[0] : item?.Sub_Catagory ?? "");
const defaultParent = $derived(item?.Parent ?? "");
const defaultHasParent = $derived(!!item?.Has_Parent);
const defaultStock = $derived(!!item?.Stock);
let skuValue = $state("");
let showScanner = $state(false);
let scanError = $state<string | null>(null);
let videoEl = $state<HTMLVideoElement | undefined>(undefined);
let stream: MediaStream | null = null;
let scanRAF = 0;
$effect(() => {
skuValue = defaultSku;
});
async function ensureBarcodeDetector(): Promise<boolean> {
if (typeof globalThis === "undefined" || "BarcodeDetector" in globalThis) return true;
try {
const mod = await import("barcode-detector-api-polyfill");
(globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector =
mod.BarcodeDetector;
return true;
} catch {
return false;
}
}
async function startScan() {
scanError = null;
const hasDetector = await ensureBarcodeDetector();
if (!hasDetector) {
scanError = "Failed to load barcode scanner. Try again or use Chrome/Edge.";
showScanner = true;
return;
}
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 } },
});
showScanner = true;
} catch (e) {
scanError = e instanceof Error ? e.message : "Could not access camera.";
showScanner = true;
}
}
$effect(() => {
if (!showScanner || !stream || !videoEl) return;
videoEl.srcObject = stream;
videoEl.play().then(() => scanLoop()).catch(() => {});
return () => {
if (videoEl.srcObject) videoEl.srcObject = null;
};
});
const SCAN_INTERVAL_MS = 300;
function scanLoop() {
if (!showScanner || !videoEl || !("BarcodeDetector" in globalThis)) return;
const detector = new (globalThis as unknown as { BarcodeDetector: typeof BarcodeDetector }).BarcodeDetector();
let lastScan = 0;
async function detect() {
if (!showScanner || !videoEl) return;
if (videoEl.readyState < 2) {
scanRAF = requestAnimationFrame(detect);
return;
}
const now = Date.now();
if (now - lastScan >= SCAN_INTERVAL_MS) {
lastScan = now;
try {
const barcodes = await detector.detect(videoEl);
if (barcodes.length > 0) {
skuValue = barcodes[0].rawValue;
stopScan();
return;
}
} catch {
// ignore frame errors
}
}
scanRAF = requestAnimationFrame(detect);
}
detect();
}
function stopScan() {
showScanner = false;
scanError = null;
if (scanRAF) {
cancelAnimationFrame(scanRAF);
scanRAF = 0;
}
if (stream) {
stream.getTracks().forEach((t) => t.stop());
stream = null;
}
if (videoEl && videoEl.srcObject) {
videoEl.srcObject = null;
}
}
</script>
<form
method="POST"
action={action}
enctype="multipart/form-data"
use:enhance={() => async ({ update, result }) => {
await update();
if (result.type === "success" && result.data?.success) {
onSuccess?.();
}
}}
class="flex flex-col gap-4"
>
{#if isEdit}
<input type="hidden" name="id" value={item!.id} />
{/if}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="Item">Item</Label>
<Input id="Item" name="Item" value={defaultItem} required />
</div>
<div class="space-y-2">
<Label for="SKU">SKU</Label>
<div class="flex gap-2">
<Input id="SKU" name="SKU" bind:value={skuValue} class="flex-1" />
<Button type="button" variant="outline" onclick={startScan} title="Scan barcode">Scan</Button>
</div>
</div>
</div>
{#if showScanner}
<div
class="bg-background/95 fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 p-4 backdrop-blur-sm"
role="dialog"
aria-label="Scan barcode"
>
<div class="flex w-full max-w-sm flex-col gap-2">
{#if scanError}
<p class="text-destructive text-sm">{scanError}</p>
{:else}
<video
bind:this={videoEl}
class="border-input w-full rounded-lg border"
muted
playsinline
></video>
{/if}
<div class="flex gap-2">
<Button type="button" variant="outline" class="flex-1" onclick={stopScan}>
Cancel
</Button>
</div>
</div>
</div>
{/if}
<div class="space-y-2">
<Label for="Description">Description</Label>
<Input id="Description" name="Description" value={defaultDesc} />
</div>
<div class="space-y-2">
<Label for="Image">Image</Label>
{#if isEdit && item?.Image}
<div class="flex flex-wrap items-center gap-3">
<img
src={itemImageUrl(item!.id, item!.Image)}
alt=""
class="size-20 rounded border object-cover"
/>
<span class="text-muted-foreground text-sm">Replace:</span>
</div>
{/if}
<Input id="Image" name="Image" type="file" accept="image/*" />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="UOM">UOM</Label>
<select
id="UOM"
name="UOM"
class="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-xs"
>
<option value=""></option>
{#each UOM_OPTIONS as o}
<option value={o} selected={defaultUom === o}>{o}</option>
{/each}
</select>
</div>
<div class="space-y-2">
<Label for="Cost">Cost</Label>
<Input id="Cost" name="Cost" type="number" step="any" value={defaultCost} />
</div>
</div>
<div class="space-y-2">
<Label for="Vendor">Vendor</Label>
<Input id="Vendor" name="Vendor" value={defaultVendor} />
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="Catagory">Catagory</Label>
<select
id="Catagory"
name="Catagory"
class="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-xs"
>
<option value=""></option>
{#each CATAGORY_OPTIONS as o}
<option value={o} selected={defaultCat === o}>{o}</option>
{/each}
</select>
</div>
<div class="space-y-2">
<Label for="Sub_Catagory">Sub_Catagory</Label>
<select
id="Sub_Catagory"
name="Sub_Catagory"
class="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-xs"
>
<option value=""></option>
{#each SUB_CATAGORY_OPTIONS as o}
<option value={o} selected={defaultSub === o}>{o}</option>
{/each}
</select>
</div>
</div>
<div class="space-y-2">
<Label for="Parent">Parent</Label>
<select
id="Parent"
name="Parent"
class="border-input bg-background flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-xs"
>
<option value=""></option>
{#each parentOptions as p}
<option value={p.id} selected={defaultParent === p.id}>
{p.Item || p.SKU || p.id}
</option>
{/each}
</select>
</div>
<div class="flex flex-wrap gap-6">
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
name="Has_Parent"
value="on"
checked={defaultHasParent}
class="border-input size-4 rounded"
/>
Has Parent
</label>
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
name="Stock"
value="on"
checked={defaultStock}
class="border-input size-4 rounded"
/>
Stock
</label>
</div>
{#if error}
<p class="text-destructive text-sm">{error}</p>
{/if}
<div class="flex justify-end gap-2">
<Button type="submit">{submitLabel}</Button>
</div>
</form>
-250
View File
@@ -1,250 +0,0 @@
<script lang="ts">
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import { Card, CardContent } from "$lib/components/ui/card";
import * as Dialog from "$lib/components/ui/dialog";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { Input } from "$lib/components/ui/input";
import { enhance } from "$app/forms";
import { goto } from "$app/navigation";
import BarcodeScanDialog from "$lib/components/BarcodeScanDialog.svelte";
import ItemForm from "./ItemForm.svelte";
import type { StackqItem } from "$lib/types/items";
let {
data,
searchFromUrl = "",
}: {
data: {
items: StackqItem[];
itemsForParent: { id: string; Item: string; SKU: string }[];
};
searchFromUrl?: string;
} = $props();
let dialogOpen = $state(false);
let scanDialogOpen = $state(false);
let editingItem = $state<StackqItem | null>(null);
let formWhich = $state<"create" | "update">("create");
let searchQuery = $state(searchFromUrl);
const items = $derived(data?.items ?? []);
const itemsForParent = $derived(data?.itemsForParent ?? []);
const filteredItems = $derived(
(() => {
const q = searchQuery.trim().toLowerCase();
if (!q) return items;
return items.filter((i) => {
const item = (i.Item ?? "").toLowerCase();
const sku = (i.SKU ?? "").toLowerCase();
const desc = (i.Description ?? "").toLowerCase();
const vendor = (i.Vendor ?? "").toLowerCase();
const cat = Array.isArray(i.Catagory) ? i.Catagory.join(" ").toLowerCase() : (i.Catagory ?? "").toLowerCase();
const sub = Array.isArray(i.Sub_Catagory) ? i.Sub_Catagory.join(" ").toLowerCase() : (i.Sub_Catagory ?? "").toLowerCase();
return item.includes(q) || sku.includes(q) || desc.includes(q) || vendor.includes(q) || cat.includes(q) || sub.includes(q);
});
})()
);
const PB_BASE = "https://pocketbase.ccllc.pro";
const COLLECTION = "Stackq_Items";
function imageUrl(item: StackqItem): string | null {
if (!item.Image) return null;
return `${PB_BASE}/api/files/${COLLECTION}/${item.id}/${item.Image}`;
}
function asList(val: string | string[] | undefined): string[] {
if (val == null) return [];
return Array.isArray(val) ? val : [val];
}
function formatDate(iso: string) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString(undefined, {
dateStyle: "short",
timeStyle: "short",
});
} catch {
return iso;
}
}
function openCreate() {
editingItem = null;
formWhich = "create";
dialogOpen = true;
}
function openEdit(item: StackqItem) {
editingItem = item;
formWhich = "update";
dialogOpen = true;
}
function onFormSuccess() {
dialogOpen = false;
editingItem = null;
}
function onScanResult(value: string) {
const sku = value.trim();
const found = items.find((i) => (i.SKU ?? "").trim() === sku);
if (found) goto("/items/" + found.id);
else goto("/items?q=" + encodeURIComponent(sku));
}
const formError = $derived(
data?.form === "create" || data?.form === "update" ? (data?.error as string | undefined) ?? null : null
);
$effect(() => {
if (formError && (data?.form === "create" || data?.form === "update")) {
dialogOpen = true;
}
});
$effect(() => {
if (searchFromUrl) searchQuery = searchFromUrl;
});
</script>
<svelte:head>
<title>Items Stackq</title>
</svelte:head>
<main class="container mx-auto flex flex-col gap-4 px-4 py-6 sm:gap-6 sm:px-6 sm:py-8 md:max-w-full">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-2 min-w-0">
<a href="/" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to main">Back</a>
<h1 class="text-xl font-semibold truncate sm:text-2xl">Stackq Items</h1>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
<div class="w-full sm:w-64 md:w-80 sm:min-w-0">
<Input
type="search"
placeholder="Search items, SKU…"
bind:value={searchQuery}
class="h-11 w-full sm:h-9"
/>
</div>
<span class="text-muted-foreground text-sm shrink-0">
{filteredItems.length}{filteredItems.length !== items.length ? ` of ${items.length}` : ""} items
</span>
<div class="flex gap-2">
<Button variant="outline" onclick={() => (scanDialogOpen = true)} title="Scan barcode to look up item" class="min-h-11 flex-1 sm:flex-none">
Scan
</Button>
<Button onclick={openCreate} class="min-h-11 flex-1 sm:flex-none">
New item
</Button>
</div>
</div>
</div>
{#if filteredItems.length === 0}
<Card>
<CardContent class="flex h-32 items-center justify-center">
<p class="text-muted-foreground text-center text-sm">
{items.length === 0
? "No items. Create one or check PocketBase collection name and auth."
: "No items match your search."}
</p>
</CardContent>
</Card>
{:else}
<div class="flex flex-col gap-2">
{#each filteredItems as item (item.id)}
<Card class="overflow-hidden">
<CardContent class="flex flex-row items-center gap-3 p-3 md:gap-4 md:px-4 md:py-3">
<div class="size-12 shrink-0 overflow-hidden rounded border">
{#if imageUrl(item)}
<a
href={imageUrl(item)!}
target="_blank"
rel="noopener noreferrer"
class="block size-full"
>
<img
src={imageUrl(item)!}
alt=""
class="size-full object-cover"
loading="lazy"
/>
</a>
{:else}
<span class="text-muted-foreground flex size-full items-center justify-center text-xs"></span>
{/if}
</div>
<div class="min-w-0 flex-1">
<a href="/items/{item.id}" class="font-medium hover:underline">{item.Item ?? "—"}</a>
<span class="text-muted-foreground font-mono text-xs"> · {item.SKU ?? "—"}</span>
{#if item.Description}
<span class="text-muted-foreground ml-1 truncate text-xs">· {item.Description}</span>
{/if}
</div>
<div class="flex shrink-0 items-center gap-2 text-xs">
<Badge variant="outline">{item.UOM ?? "—"}</Badge>
<span class="tabular-nums">{item.Cost != null ? Number(item.Cost).toLocaleString() : "—"}</span>
{#if item.Stock}<Badge variant="default" class="text-xs">Stock</Badge>{/if}
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger
class="border-0 bg-transparent hover:bg-muted inline-flex min-h-9 min-w-9 shrink-0 items-center justify-center rounded-md text-sm"
>
Actions
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => goto("/items/" + item.id)}>View</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => openEdit(item)}>Edit</DropdownMenu.Item>
<form
id="delete-form-{item.id}"
method="POST"
action="?/delete"
use:enhance
class="hidden"
>
<input type="hidden" name="id" value={item.id} />
</form>
<DropdownMenu.Item
class="text-destructive focus:text-destructive cursor-pointer"
onclick={() => {
if (confirm("Delete this item? This cannot be undone.")) {
document.getElementById("delete-form-" + item.id)?.requestSubmit();
}
}}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</CardContent>
</Card>
{/each}
</div>
{/if}
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="max-h-[90dvh] w-[calc(100vw-2rem)] max-w-[500px] overflow-y-auto rounded-lg sm:w-full">
<Dialog.Header>
<Dialog.Title>{editingItem ? "Edit item" : "New item"}</Dialog.Title>
<Dialog.Description>
{editingItem ? "Update the item below." : "Add a new Stackq item."}
</Dialog.Description>
</Dialog.Header>
<ItemForm
item={editingItem}
itemsForParent={itemsForParent}
action={editingItem ? "?/update" : "?/create"}
submitLabel={editingItem ? "Update" : "Create"}
error={formError ?? null}
onSuccess={onFormSuccess}
/>
</Dialog.Content>
</Dialog.Root>
<BarcodeScanDialog bind:open={scanDialogOpen} onResult={onScanResult} title="Scan to look up item" />
</main>

Some files were not shown because too many files have changed in this diff Show More