Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e7444d88 | ||
|
|
65d83ddec7 | ||
|
|
54a51752ac | ||
|
|
9c15b81409 | ||
|
|
4a03ab8e1a |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
_commit: v0.7.0
|
_commit: v0.8.0
|
||||||
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
_src_path: https://git.kotikot.com/templates/frontend-svelte.git
|
||||||
install_deps: true
|
install_deps: true
|
||||||
layout: root
|
layout: root
|
||||||
|
|||||||
+4
-5
@@ -7,20 +7,19 @@
|
|||||||
"!.svelte-kit",
|
"!.svelte-kit",
|
||||||
"!build",
|
"!build",
|
||||||
"!.mcp.json",
|
"!.mcp.json",
|
||||||
|
<<<<<<< before updating
|
||||||
"!.impeccable",
|
"!.impeccable",
|
||||||
"!src/lib/components/ui",
|
"!src/lib/components/ui",
|
||||||
"!src/routes/claddcn.css",
|
"!src/routes/claddcn.css",
|
||||||
"!!src/lib/api/schema.d.ts",
|
"!!src/lib/api/schema.d.ts",
|
||||||
"!api"
|
"!api"
|
||||||
|
=======
|
||||||
|
"!src/lib/components/ui"
|
||||||
|
>>>>>>> after updating
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"javascript": { "globals": ["Bun"] },
|
"javascript": { "globals": ["Bun"] },
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
|
||||||
"includes": ["src/lib/components/ui/**"],
|
|
||||||
"linter": { "enabled": false },
|
|
||||||
"assist": { "enabled": false }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"includes": ["src/lib/utils.ts"],
|
"includes": ["src/lib/utils.ts"],
|
||||||
"linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } }
|
"linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } }
|
||||||
|
|||||||
@@ -40,9 +40,13 @@
|
|||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
|
<<<<<<< before updating
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
"openapi-typescript": "^7.13.0",
|
"openapi-typescript": "^7.13.0",
|
||||||
"shadcn-svelte": "^1.3.0",
|
"shadcn-svelte": "^1.3.0",
|
||||||
|
=======
|
||||||
|
"shadcn-svelte": "^1.7.0",
|
||||||
|
>>>>>>> after updating
|
||||||
"svelte": "^5.57.1",
|
"svelte": "^5.57.1",
|
||||||
"svelte-check": "^4.7.6",
|
"svelte-check": "^4.7.6",
|
||||||
"svelte-sonner": "^1.1.0",
|
"svelte-sonner": "^1.1.0",
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
import { Popover } from "bits-ui";
|
import { Popover } from "bits-ui";
|
||||||
import { period } from "#lib/period.svelte.js";
|
import { period } from "#lib/period.svelte.js";
|
||||||
|
import { ui } from "#lib/ui.svelte.js";
|
||||||
|
|
||||||
let open = $state(false);
|
|
||||||
let start = $state(period.start);
|
let start = $state(period.start);
|
||||||
let end = $state(period.end);
|
let end = $state(period.end);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open) {
|
if (ui.periodOpen) {
|
||||||
({ start, end } = period);
|
({ start, end } = period);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (start && end && start <= end) {
|
if (start && end && start <= end) {
|
||||||
period.set({ end, start });
|
period.set({ end, start });
|
||||||
open = false;
|
ui.periodOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
>
|
>
|
||||||
<ChevronLeft class="size-4" />
|
<ChevronLeft class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
<Popover.Root bind:open>
|
<Popover.Root bind:open={ui.periodOpen}>
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
class="flex h-8 items-center gap-2 px-2 font-medium text-[13px] hover:bg-black/10"
|
class="flex h-8 items-center gap-2 px-2 font-medium text-[13px] hover:bg-black/10"
|
||||||
title="Choose period"
|
title="Choose period"
|
||||||
@@ -59,11 +59,16 @@
|
|||||||
: ""}"
|
: ""}"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
period.set(preset.range);
|
period.set(preset.range);
|
||||||
open = false;
|
ui.periodOpen = false;
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{preset.label}
|
{preset.label}
|
||||||
|
{#if preset.key || preset.hint}
|
||||||
|
<span class="kbd"
|
||||||
|
>{preset.key ? `p ${preset.key}` : preset.hint}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -76,6 +81,7 @@
|
|||||||
Start
|
Start
|
||||||
<input
|
<input
|
||||||
class="cladd-cut cladd-focusable-input h-8 rounded-cladd-sm px-2 text-[13px] text-cladd-fg"
|
class="cladd-cut cladd-focusable-input h-8 rounded-cladd-sm px-2 text-[13px] text-cladd-fg"
|
||||||
|
id="period-start"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={start}
|
bind:value={start}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,6 +36,18 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setPreset(key: string | undefined) {
|
||||||
|
const preset = period.presets().find((item) => item.key === key);
|
||||||
|
if (preset) {
|
||||||
|
period.set(preset.range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCustomPeriod() {
|
||||||
|
ui.periodOpen = true;
|
||||||
|
setTimeout(() => document.getElementById("period-start")?.focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
function openOld(sticky = false) {
|
function openOld(sticky = false) {
|
||||||
location.assign(oldUiUrl(page.url.href, sticky));
|
location.assign(oldUiUrl(page.url.href, sticky));
|
||||||
}
|
}
|
||||||
@@ -148,6 +160,27 @@
|
|||||||
label: "Current period",
|
label: "Current period",
|
||||||
run: () => period.reset(),
|
run: () => period.reset(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: "Period",
|
||||||
|
keys: "p p",
|
||||||
|
label: "Choose a period",
|
||||||
|
run: () => (ui.periodOpen = true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "Period",
|
||||||
|
keys: "p c",
|
||||||
|
label: "Custom period: type the dates",
|
||||||
|
run: openCustomPeriod,
|
||||||
|
},
|
||||||
|
...period
|
||||||
|
.presets()
|
||||||
|
.filter((preset) => preset.key)
|
||||||
|
.map((preset) => ({
|
||||||
|
group: "Period",
|
||||||
|
keys: `p ${preset.key}`,
|
||||||
|
label: `Period: ${preset.label}`,
|
||||||
|
run: () => setPreset(preset.key),
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
group: "Global",
|
group: "Global",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
onclick={() => byCurrency || onmode()}
|
onclick={() => byCurrency || onmode()}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
By currency<span class="kbd max-sm:hidden">p</span>
|
By currency<span class="kbd max-sm:hidden">v</span>
|
||||||
</button>
|
</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+16
-5
@@ -118,12 +118,23 @@ export function periodOf(value: string, view: ViewRange): Range {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNITS: ViewRange[] = ["1M", "3M", "6M", "1Y"];
|
||||||
|
|
||||||
|
function unitOf(range: Range, view: ViewRange): ViewRange | null {
|
||||||
|
const units = isDynamic(view) ? UNITS : [view, ...UNITS];
|
||||||
|
return (
|
||||||
|
units.find((unit) => {
|
||||||
|
const aligned = periodOf(range.start, unit);
|
||||||
|
return aligned.start === range.start && range.end <= aligned.end;
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
|
export function shiftPeriod(range: Range, view: ViewRange, dir: 1 | -1): Range {
|
||||||
const fixed = isDynamic(view) ? "1M" : view;
|
const unit = unitOf(range, view);
|
||||||
const aligned = periodOf(range.start, fixed);
|
if (unit) {
|
||||||
if (aligned.start === range.start && aligned.end === range.end) {
|
const anchor = dir === 1 ? periodOf(range.start, unit).end : range.start;
|
||||||
const anchor = dir === 1 ? addDays(range.end, 1) : addDays(range.start, -1);
|
return periodOf(addDays(anchor, dir), unit);
|
||||||
return periodOf(anchor, fixed);
|
|
||||||
}
|
}
|
||||||
const days = daysBetween(range.start, range.end) + 1;
|
const days = daysBetween(range.start, range.end) + 1;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const STORAGE_KEY = "ffui.period";
|
|||||||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
|
hint?: string;
|
||||||
|
key?: string;
|
||||||
label: string;
|
label: string;
|
||||||
range: Range;
|
range: Range;
|
||||||
}
|
}
|
||||||
@@ -113,31 +115,44 @@ class Period {
|
|||||||
if (!isDynamic(view)) {
|
if (!isDynamic(view)) {
|
||||||
const prev = shiftPeriod(this.range, view, -1);
|
const prev = shiftPeriod(this.range, view, -1);
|
||||||
const next = shiftPeriod(this.range, view, 1);
|
const next = shiftPeriod(this.range, view, 1);
|
||||||
presets.push({ label: periodLabel(prev, view), range: prev });
|
presets.push({ hint: "[", label: periodLabel(prev, view), range: prev });
|
||||||
presets.push({ label: periodLabel(next, view), range: next });
|
presets.push({ hint: "]", label: periodLabel(next, view), range: next });
|
||||||
}
|
}
|
||||||
const current = periodOf(now, view);
|
const current = periodOf(now, view);
|
||||||
presets.push({
|
presets.push({
|
||||||
|
key: "t",
|
||||||
label: `Today (${periodLabel(current, view)})`,
|
label: `Today (${periodLabel(current, view)})`,
|
||||||
range: current,
|
range: current,
|
||||||
});
|
});
|
||||||
presets.push({
|
presets.push({
|
||||||
|
key: "7",
|
||||||
label: "Last seven days",
|
label: "Last seven days",
|
||||||
range: { end: now, start: addDays(now, -7) },
|
range: { end: now, start: addDays(now, -7) },
|
||||||
});
|
});
|
||||||
presets.push({
|
presets.push({
|
||||||
|
key: "3",
|
||||||
label: "Last thirty days",
|
label: "Last thirty days",
|
||||||
range: { end: now, start: addDays(now, -30) },
|
range: { end: now, start: addDays(now, -30) },
|
||||||
});
|
});
|
||||||
presets.push({ label: "Month to date", range: periodOf(now, "MTD") });
|
presets.push({
|
||||||
presets.push({ label: "Year to date", range: periodOf(now, "YTD") });
|
key: "m",
|
||||||
|
label: "Month to date",
|
||||||
|
range: periodOf(now, "MTD"),
|
||||||
|
});
|
||||||
|
presets.push({
|
||||||
|
key: "y",
|
||||||
|
label: "Year to date",
|
||||||
|
range: periodOf(now, "YTD"),
|
||||||
|
});
|
||||||
const year = Number(now.slice(0, 4)) - 1;
|
const year = Number(now.slice(0, 4)) - 1;
|
||||||
presets.push({
|
presets.push({
|
||||||
|
key: "l",
|
||||||
label: `Previous year (${year})`,
|
label: `Previous year (${year})`,
|
||||||
range: { end: `${year}-12-31`, start: `${year}-01-01` },
|
range: { end: `${year}-12-31`, start: `${year}-01-01` },
|
||||||
});
|
});
|
||||||
if (this.first) {
|
if (this.first) {
|
||||||
presets.push({
|
presets.push({
|
||||||
|
key: "a",
|
||||||
label: "Everything",
|
label: "Everything",
|
||||||
range: { end: now, start: this.first },
|
range: { end: now, start: this.first },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ export const runwayQueries = {
|
|||||||
bills: (start: string, end: string) => ({
|
bills: (start: string, end: string) => ({
|
||||||
queryFn: () => all<Bill>("/v1/bills", { end, start }),
|
queryFn: () => all<Bill>("/v1/bills", { end, start }),
|
||||||
queryKey: ["subscriptions", "runway", start, end],
|
queryKey: ["subscriptions", "runway", start, end],
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
}),
|
}),
|
||||||
paid: () => ({
|
paid: () => ({
|
||||||
queryFn: async () => toPayments(await search("has_any_bill:true")),
|
queryFn: async () => toPayments(await search("has_any_bill:true")),
|
||||||
queryKey: ["subscriptions", "payments"],
|
queryKey: ["subscriptions", "payments"],
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
}),
|
}),
|
||||||
unlinked: (day: string) => {
|
unlinked: (day: string) => {
|
||||||
const after = addDays(day, -UNLINKED_DAYS);
|
const after = addDays(day, -UNLINKED_DAYS);
|
||||||
|
|||||||
@@ -208,11 +208,7 @@ function occurrences(bill: Bill, day: string) {
|
|||||||
return (bill.pay_dates ?? [])
|
return (bill.pay_dates ?? [])
|
||||||
.map((raw) => raw.slice(0, 10))
|
.map((raw) => raw.slice(0, 10))
|
||||||
.filter((date) => !end || date <= end)
|
.filter((date) => !end || date <= end)
|
||||||
.filter(
|
.filter((date) => !paid.some((p) => Math.abs(daysBetween(p, date)) < early))
|
||||||
(date) =>
|
|
||||||
date < day ||
|
|
||||||
!paid.some((p) => p <= date && daysBetween(p, date) < early)
|
|
||||||
)
|
|
||||||
.map((date) => ({ date: date < day ? day : date, overdue: date < day }));
|
.map((date) => ({ date: date < day ? day : date, overdue: date < day }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Ui {
|
|||||||
palette = $state(false);
|
palette = $state(false);
|
||||||
paletteQuery = $state("");
|
paletteQuery = $state("");
|
||||||
quickEntry = $state(false);
|
quickEntry = $state(false);
|
||||||
|
periodOpen = $state(false);
|
||||||
mobileNav = $state(false);
|
mobileNav = $state(false);
|
||||||
collapsed = $state(stored(SIDEBAR_KEY));
|
collapsed = $state(stored(SIDEBAR_KEY));
|
||||||
notes = $state(stored(NOTES_KEY));
|
notes = $state(stored(NOTES_KEY));
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "Currencies",
|
group: "Currencies",
|
||||||
keys: "p",
|
keys: "P",
|
||||||
label: "Make primary currency",
|
label: "Make primary currency",
|
||||||
run: () => nav.current && makePrimary(nav.current),
|
run: () => nav.current && makePrimary(nav.current),
|
||||||
},
|
},
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem onSelect={() => makePrimary(item)}>
|
<ContextMenuItem onSelect={() => makePrimary(item)}>
|
||||||
Make primary
|
Make primary
|
||||||
<span class="ml-auto kbd">p</span>
|
<span class="ml-auto kbd">P</span>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<MenuDelete {arm} id={item.id} run={() => remove(item)} />
|
<MenuDelete {arm} id={item.id} run={() => remove(item)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -296,7 +296,7 @@
|
|||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
group: "Runway",
|
group: "Runway",
|
||||||
keys: "p",
|
keys: "v",
|
||||||
label: perCurrency ? "Group by card" : "Group by currency",
|
label: perCurrency ? "Group by card" : "Group by currency",
|
||||||
run: toggleMode,
|
run: toggleMode,
|
||||||
},
|
},
|
||||||
@@ -491,7 +491,9 @@
|
|||||||
{@const ends = bill.end_date?.slice(0, 10)}
|
{@const ends = bill.end_date?.slice(0, 10)}
|
||||||
{@const last = lastOf(item)}
|
{@const last = lastOf(item)}
|
||||||
{@const who = payer(item)}
|
{@const who = payer(item)}
|
||||||
{@const stale = staleSince(bill, last, day)}
|
{@const stale = paid.isSuccess
|
||||||
|
? staleSince(bill, last, day)
|
||||||
|
: null}
|
||||||
{@const extra = found.get(item.id) ?? []}
|
{@const extra = found.get(item.id) ?? []}
|
||||||
{@const armedHere = arm.key?.endsWith(`:${item.id}`)}
|
{@const armedHere = arm.key?.endsWith(`:${item.id}`)}
|
||||||
<tr
|
<tr
|
||||||
@@ -567,7 +569,7 @@
|
|||||||
billIds={manualCards.picking.ids}
|
billIds={manualCards.picking.ids}
|
||||||
cards={cardList}
|
cards={cardList}
|
||||||
/>
|
/>
|
||||||
{:else if bill.active}
|
{:else if bill.active && paid.isSuccess}
|
||||||
{#if who.card}
|
{#if who.card}
|
||||||
<span class="text-cladd-fg-soft"
|
<span class="text-cladd-fg-soft"
|
||||||
>{who.card.name}</span
|
>{who.card.name}</span
|
||||||
|
|||||||
Reference in new issue
Block a user