Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"dotenv": "^17.3.1",
"gsap": "^3.14.2",
"mode-watcher": "^1.1.0",
"pg": "^8.18.0",
"pg": "^8.19.0",
"runed": "^0.37.1",
"svelte-toolbelt": "^0.10.6",
"valibot": "^1.2.0"
Expand All @@ -52,10 +52,10 @@
"@svelte-put/qr": "^2.1.1",
"@sveltejs/adapter-node": "^5.5.3",
"@sveltejs/enhanced-img": "^0.10.3",
"@sveltejs/kit": "^2.53.0",
"@sveltejs/kit": "^2.53.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.0",
"@tailwindcss/vite": "^4.2.1",
"@tanstack/table-core": "^8.21.3",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.8",
Expand All @@ -72,14 +72,14 @@
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.53.3",
"svelte": "^5.53.5",
"svelte-check": "^4.4.3",
"svelte-render-scan": "^1.1.0",
"svelte-sonner": "^1.0.7",
"sveltekit-sse": "^0.14.3",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.2.0",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
Expand Down
392 changes: 203 additions & 189 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/lib/api/auth.remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ export const requireAuth = query(() => requireAuthServer());
export const signout = form(async () => {
const event = getRequestEvent();
if (event.locals.session === null) {
return redirect(303, "/");
return redirect(303, "/auth?act=login");
}
invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
redirect(303, "/");
redirect(303, "/auth?act=login");
});

export const login = form(loginSchema, async (user, issues) => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/assets/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ export { default as Shield } from "phosphor-svelte/lib/ShieldIcon";
export { default as ShieldCheck } from "phosphor-svelte/lib/ShieldCheckIcon";
export { default as ShieldOff } from "phosphor-svelte/lib/ShieldSlashIcon";
export { default as Copy } from "phosphor-svelte/lib/CopyIcon";
export { default as InvoiceIcon } from "phosphor-svelte/lib/InvoiceIcon";
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
column.toggleVisibility(!!value);
}}
>
{column.id}
{column.id.replace("Formatted", "")}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Content>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const site = {
url: "https://powertrackr.up.railway.app",
ogImage: "https://powertrackr.vercel.app/og.avif",
description: "Track and reconcile electricity usage and payments.",
fullDescription:
"Record, organize, and reconcile electricity usage and payments across an account and its sub-meters.",
keywords: `svelte, monitoring-tool, energy-monitoring, sveltekit, bill-monitoring, payment-reconciliation`,
};
export type SiteConfig = typeof site;
11 changes: 4 additions & 7 deletions src/lib/types/billing-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ export type BillingInfoTableView = Omit<BillingInfo, "date" | "createdAt" | "upd
updatedAt: string;
};

export type ExtendedBillingInfoTableView = Omit<
ExtendedBillingInfo,
"date" | "createdAt" | "updatedAt"
> & {
date: string;
createdAt: string;
updatedAt: string;
export type ExtendedBillingInfoTableView = ExtendedBillingInfo & {
dateFormatted: string;
createdAtFormatted: string;
updatedAtFormatted: string;
};

export type ExtendedBillingInfo = BillingInfo & {
Expand Down
19 changes: 19 additions & 0 deletions src/lib/types/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,22 @@ export interface Stats {
formatted: string;
};
}

/* Types for the stats payload. Keep these narrow and explicit so the cache is portable. */
export type EnergyUsed = {
total: number;
formatted: string;
energyUnit: string;
};

export type PaymentsAmount = {
total: number;
formatted: string;
};

export type StatsPayload = {
userCount: number;
energyUsed: EnergyUsed;
billingCount: number;
paymentsAmount: PaymentsAmount;
};
10 changes: 5 additions & 5 deletions src/lib/utils/mapper/billing-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import type {
ExtendedBillingInfo,
ExtendedBillingInfoTableView,
} from "$/types/billing-info";
import { formatDate, DateFormat, parseCalendarDate } from "$/utils/format";
import { formatDate, DateFormat } from "$/utils/format";

export function billingInfoToDto(
original: ExtendedBillingInfoTableView
): BillingInfoDTOWithSubMeters {
return {
id: original.id,
userId: original.userId,
date: parseCalendarDate(original.date),
date: original.date,
totalkWh: original.totalkWh,
balance: original.balance,
payPerkWh: original.payPerkWh,
Expand Down Expand Up @@ -43,11 +43,11 @@ export function extendedBillingInfoToTableView(
): ExtendedBillingInfoTableView {
return {
...original,
date: formatDate(original.date),
createdAt: formatDate(new Date(original.createdAt), {
dateFormatted: formatDate(original.date),
createdAtFormatted: formatDate(new Date(original.createdAt), {
format: DateFormat.DateTime,
}),
updatedAt: formatDate(new Date(original.updatedAt), {
updatedAtFormatted: formatDate(new Date(original.updatedAt), {
format: DateFormat.DateTime,
}),
};
Expand Down
3 changes: 1 addition & 2 deletions src/routes/(components)/account-settings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -578,8 +578,7 @@
<Button
type="submit"
variant="destructive"
disabled={(disable2FA.fields.code.value() &&
disable2FA.fields.code.value().length !== 6) ||
disabled={disable2FA.fields?.code?.value()?.length !== 6 ||
securityAsyncState === "processing"}
>
{#if securityAsyncState === "processing"}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/(components)/header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
callback={quickAction.callback}
billingInfo={latestBillingInfo}
bind:open={quickAction.open}
/>logo
/>
{/if}
</div>
{/key}
Expand Down
61 changes: 24 additions & 37 deletions src/routes/(components)/landing-footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { gsap } from "gsap";
import Logo from "$/components/logo.svelte";
import { site } from "$/site";
import { ChartLine, Users, Shield, Download } from "$lib/assets/icons";
import { ChartLine, Users, Shield, Download, InvoiceIcon } from "$lib/assets/icons";
import { LANDING_NAV_ITEMS, handleLandingNavClick } from ".";

let { user }: LandingFooterProps = $props();
Expand Down Expand Up @@ -122,28 +122,38 @@

<footer use:footerAttach class="relative z-10 overflow-hidden border-t border-border bg-background">
<!-- Electric grid canvas background -->
<canvas class="grid-canvas pointer-events-none absolute inset-0 h-full w-full" aria-hidden="true"
<canvas class="grid-canvas pointer-events-none absolute inset-0 size-full" aria-hidden="true"
></canvas>

<div class="relative z-10 container mx-auto px-4 py-16">
<div class="grid gap-12 md:grid-cols-2 lg:grid-cols-4">
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4 lg:gap-12">
<!-- Brand Column -->
<div class="footer-col space-y-4">
<div class="footer-col flex flex-col gap-4">
<Logo variant="ghost" class="px-0 md:pl-0!" viewTransitionName="logo-footer" />
<p class="text-sm leading-relaxed text-muted-foreground">
{site.description}
</p>
<p class="text-sm leading-relaxed text-muted-foreground">
Software-focused tracking and billing for practical expense allocation.
{site.fullDescription}
</p>
</div>

<!-- Features -->
<div class="footer-col flex flex-col gap-4">
<h3 class="text-sm font-semibold">Features</h3>
<ul class="flex flex-col gap-0">
{#each [{ Icon: ChartLine, label: "Billing Summaries" }, { Icon: InvoiceIcon, label: "Sub‑Metering & Auto‑Billing" }, { Icon: Users, label: "User Accounts" }, { Icon: Shield, label: "Input Validation" }, { Icon: Download, label: "Import & Export" }] as { Icon, label }}
<li class="flex h-9 items-center gap-2 text-sm text-muted-foreground">
<Icon class="size-4 shrink-0" />
<span>{label}</span>
</li>
{/each}
</ul>
</div>

<!-- Navigation Links -->
<div class="footer-col space-y-4">
<div class="footer-col flex flex-col gap-4">
<h3 class="text-sm font-semibold">Navigation</h3>
<ul class="space-y-3">
<ul class="flex flex-col gap-0">
{#each LANDING_NAV_ITEMS as item}
<li>
<li class="flex h-9 items-center">
<a
href={item.href}
onclick={(e) => handleLandingNavClick(e, item.href)}
Expand All @@ -156,35 +166,12 @@
</ul>
</div>

<!-- Features -->
<div class="footer-col space-y-4">
<h3 class="text-sm font-semibold">Features</h3>
<ul class="space-y-3">
<li class="flex items-center gap-2 text-sm text-muted-foreground">
<ChartLine class="h-4 w-4 shrink-0" />
<span>Billing Summaries</span>
</li>
<li class="flex items-center gap-2 text-sm text-muted-foreground">
<Users class="h-4 w-4 shrink-0" />
<span>User Accounts</span>
</li>
<li class="flex items-center gap-2 text-sm text-muted-foreground">
<Shield class="h-4 w-4 shrink-0" />
<span>Input Validation</span>
</li>
<li class="flex items-center gap-2 text-sm text-muted-foreground">
<Download class="h-4 w-4 shrink-0" />
<span>Import &amp; Export</span>
</li>
</ul>
</div>

<!-- Use Cases -->
<div class="footer-col space-y-4">
<div class="footer-col flex flex-col gap-4">
<h3 class="text-sm font-semibold">Built For</h3>
<ul class="space-y-3">
<ul class="flex flex-col gap-0">
{#each ["Multi-Tenant Buildings", "Homeowners with Rentals", "Property Managers"] as item}
<li class="text-sm text-muted-foreground">{item}</li>
<li class="flex h-9 items-center text-sm text-muted-foreground">{item}</li>
{/each}
</ul>
</div>
Expand Down
14 changes: 13 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,20 @@
} from "./(components)";
import { ScrollParallax } from "$lib/motion-core";
import { getAuthUser } from "$/api/auth.remote";
import { onMount } from "svelte";

let { user, session } = await getAuthUser();
let { user, session } = $state<App.Locals>({
user: null,
session: null,
});

onMount(() => {
try {
getAuthUser().then((data) => ([user, session] = [data.user, data.session]));
} catch (e) {
console.warn("Failed to fetch user data:", e);
}
});
</script>

<div class="relative min-h-screen overflow-hidden bg-background">
Expand Down
33 changes: 28 additions & 5 deletions src/routes/auth/(components)/checkpoint-2fa.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import { Button } from "$/components/ui/button/index.js";
import { Loader } from "$lib/assets/icons.js";
import * as InputOTP from "$/components/ui/input-otp";
import { checkpoint2FA } from "$/api/auth.remote";
import { showLoading, showSuccess, showWarning } from "$/components/toast";
import { checkpoint2FA, signout } from "$/api/auth.remote";
import { showError, showLoading, showSuccess, showWarning } from "$/components/toast";
import { toast } from "svelte-sonner";
import { isHttpError } from "@sveltejs/kit";
import { REGEXP_ONLY_DIGITS } from "bits-ui";
Expand Down Expand Up @@ -89,9 +89,7 @@

<Button
type="submit"
disabled={(checkpoint2FA.fields.code.value() &&
checkpoint2FA.fields.code.value().length !== 6) ||
status === "processing"}
disabled={checkpoint2FA.fields?.code.value()?.length !== 6 || status === "processing"}
class="min-w-32"
>
{#if status === "processing"}
Expand All @@ -103,3 +101,28 @@
</Button>
</FieldGroup>
</form>
<div class="mt-4 flex justify-center">
<form
{...signout.enhance(async ({ submit }) => {
const toastId = showLoading("Logging out...");
try {
await submit();
showSuccess("Logged out successfully");
} catch (e) {
showError("Failed to logout. Please try again.");
} finally {
toast.dismiss(toastId);
}
})}
class="flex"
>
<Button
type="submit"
variant="link"
disabled={status === "processing"}
aria-label="Login with another account"
>
Login with another account
</Button>
</form>
</div>
3 changes: 1 addition & 2 deletions src/routes/auth/(components)/setup-2fa-form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,7 @@
<Button
type="submit"
class="w-full"
disabled={(verify2FA.fields.code.value() &&
verify2FA.fields.code.value().length !== 6) ||
disabled={verify2FA.fields?.code?.value()?.length !== 6 ||
asyncState === "processing"}
>
{#if asyncState === "processing"}
Expand Down
7 changes: 5 additions & 2 deletions src/routes/history/(components)/billing-info-form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,12 @@

{#if subMeter?.reading != 0}
<div class="text-sm text-muted-foreground">
Consumption: {isNaN(currentMeter.reading) || currentMeter.reading === 0
Consumption: {(currentMeter.reading && isNaN(currentMeter.reading)) ||
currentMeter.reading === 0
? formatEnergy(0)
: formatEnergy(currentMeter.reading - subMeter.reading)}
: formatEnergy(
currentMeter.reading ? currentMeter.reading - subMeter.reading : 0
)}
</div>
{/if}
</Field.Group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { Loader, Trash2, View, Pencil, Ticket } from "$/assets/icons";
import { Table, TableBody, TableCell, TableRow } from "$lib/components/ui/table";
import { BillingInfoForm, SubPaymentsButton } from ".";
import { formatNumber } from "$/utils/format";
import { formatDate, formatNumber } from "$/utils/format";
import type { Row } from "@tanstack/table-core";
import Button from "$/components/ui/button/button.svelte";
import * as Dialog from "$/components/ui/dialog";
Expand Down Expand Up @@ -88,7 +88,7 @@
]);

async function handle_remove_billing_info() {
if (delete_confirm_value !== row.original.date) {
if (delete_confirm_value !== formatDate(row.original.date)) {
return showInspectorWarning();
}
app_state = "processing";
Expand Down Expand Up @@ -206,7 +206,7 @@
{/if}

{#if active_dialog_content === "remove"}
{@const currentDate = row.original.date}
{@const currentDate = formatDate(row.original.date)}
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Remove Billing Info Record</Dialog.Title>
Expand Down
Loading