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.
This commit is contained in:
@@ -24,7 +24,7 @@ export const options = {
|
||||
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: "qnx61n"
|
||||
version_hash: "co0587"
|
||||
};
|
||||
|
||||
export async function get_hooks() {
|
||||
|
||||
@@ -11,6 +11,13 @@ function isHeicFile(file: File): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** 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,
|
||||
@@ -18,15 +25,15 @@ function makeOutputFile(blob: Blob, baseName: string, ext: string): File {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an image File to WebP (or JPEG on Safari where WebP isn't supported).
|
||||
* HEIC from iPhone is handled: if the image fails to load, we decode with heic2any first.
|
||||
*/
|
||||
export function convertImageToWebP(file: File, quality = 0.85): Promise<File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
|
||||
function isSafari(): boolean {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
const ua = navigator.userAgent;
|
||||
return ua.includes("Safari") && !ua.includes("Chrome");
|
||||
}
|
||||
|
||||
const tryCanvas = (sourceBlob: Blob) => {
|
||||
/** 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 = () => {
|
||||
@@ -68,28 +75,43 @@ export function convertImageToWebP(file: File, quality = 0.85): Promise<File> {
|
||||
reject(new Error("Failed to load image"));
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
tryCanvas(file);
|
||||
};
|
||||
img.onerror = async () => {
|
||||
URL.revokeObjectURL(url);
|
||||
// HEIC often fails to load in Image() on iOS; decode with heic2any then run canvas
|
||||
if (isHeicFile(file)) {
|
||||
try {
|
||||
const jpegBlob = await convertHeicBlobToJpeg(file);
|
||||
tryCanvas(jpegBlob);
|
||||
} catch (e) {
|
||||
reject(e instanceof Error ? e : new Error("Failed to convert HEIC"));
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
+28
-16
@@ -17,27 +17,39 @@
|
||||
<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>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>Scan</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>New Transaction</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<a href="/receipts" class="block">
|
||||
<Button class="h-12 min-h-12 w-full" variant="outline">New receipt</Button>
|
||||
</a>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>New Item</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
</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>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>Items</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>Transactions</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<a href="/receipts" class="block">
|
||||
<Button class="h-12 min-h-12 w-full" variant="outline">Receipts</Button>
|
||||
</a>
|
||||
<div class="text-muted-foreground flex h-12 min-h-12 w-full cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 text-sm opacity-60">
|
||||
<span>Accounts</span>
|
||||
<span class="text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -42,31 +42,41 @@
|
||||
<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>
|
||||
<div class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>Scan</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>New transaction</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<Button class="h-16 min-h-16 w-full" variant="outline" onclick={() => (receiptDialogOpen = true)}>
|
||||
New receipt
|
||||
</Button>
|
||||
<Button class="h-16 min-h-16 w-full" variant="outline" onclick={() => (dialogOpen = true)}>New Item</Button>
|
||||
<div class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>New Item</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
</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>
|
||||
<div class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>Items</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>Transactions</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
<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 class="text-muted-foreground flex h-16 w-full min-h-16 cursor-not-allowed items-center justify-between gap-2 rounded-md border border-input bg-muted/40 px-4 opacity-60">
|
||||
<span>Accounts</span>
|
||||
<span class="text-muted-foreground shrink-0 text-xs font-medium">Coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { convertImageToWebP } from "$lib/convert-image-to-webp";
|
||||
import { convertImageToWebP, isAcceptableImageFile } from "$lib/convert-image-to-webp";
|
||||
import { enhance } from "$app/forms";
|
||||
import Loader2Icon from "@lucide/svelte/icons/loader-2";
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
async function onImageChange(e: Event) {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file || !file.type.startsWith("image/")) {
|
||||
if (!file || !isAcceptableImageFile(file)) {
|
||||
imageFileName = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import * as Collapsible from "$lib/components/ui/collapsible";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { convertImageToWebP } from "$lib/convert-image-to-webp";
|
||||
import { convertImageToWebP, isAcceptableImageFile } from "$lib/convert-image-to-webp";
|
||||
import { POCKETBASE_BASE_URL } from "$lib/pocketbase";
|
||||
import { enhance } from "$app/forms";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
@@ -138,7 +138,7 @@
|
||||
async function onImageChange() {
|
||||
const input = imageInputEl;
|
||||
const file = input?.files?.[0] ?? null;
|
||||
if (!file || !file.type.startsWith("image/")) {
|
||||
if (!file || !isAcceptableImageFile(file)) {
|
||||
selectedImageFile = null;
|
||||
imageFileName = null;
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user