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
53 changes: 33 additions & 20 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ name: Deploy to GitHub Pages

on:
push:
branches: [main]
branches-ignore:
- gh-pages

permissions:
contents: read
pages: write
id-token: write
contents: write

concurrency:
group: pages
group: pages-${{ github.ref == 'refs/heads/main' && 'main' || 'preview' }}
cancel-in-progress: true

jobs:
build:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -29,22 +28,36 @@ jobs:
working-directory: pwa
run: npm ci

- name: Build
- name: Build (production)
if: github.ref == 'refs/heads/main'
working-directory: pwa
run: npm run build

- uses: actions/configure-pages@v5
- name: Build (preview)
if: github.ref != 'refs/heads/main'
working-directory: pwa
env:
VITE_BASE_PATH: /tag-notes/preview/
VITE_APP_VARIANT: preview
run: npm run build

- uses: actions/upload-pages-artifact@v4
# Both jobs publish to the same gh-pages branch: production deploys to
# its root, every other branch overwrites the shared /preview/ subpath.
# keep_files keeps each deploy from wiping the other's directory.
- name: Deploy (production)
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
path: pwa/dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: pwa/dist
destination_dir: .
keep_files: true

- name: Deploy (preview)
if: github.ref != 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: pwa/dist
destination_dir: preview
keep_files: true
1 change: 0 additions & 1 deletion pwa/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<meta name="apple-mobile-web-app-title" content="Tag Notes" />

<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/logo.svg" sizes="any" type="image/svg+xml" />
</head>
Expand Down
28 changes: 0 additions & 28 deletions pwa/public/manifest.webmanifest

This file was deleted.

2 changes: 1 addition & 1 deletion pwa/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Shell(props: { children?: JSX.Element }) {

function App() {
return (
<Router root={Shell} base="/tag-notes/">
<Router root={Shell} base={import.meta.env.BASE_URL}>
<Route path="/" component={() => <Navigate href="/notes" />} />
<Route path="/notes" component={NoteCreate} />
<Route path="/notes/history" component={NoteHistory} />
Expand Down
41 changes: 41 additions & 0 deletions pwa/src/lib/foods/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,47 @@ describe('createIndexedDbFoodStore', () => {
expect(entries.map((e) => e.id)).toEqual([b.id, a.id]);
});

it('update() overwrites fields while preserving id and createdAt', async () => {
const entry = await store.create({
name: 'A',
quantity: 1,
protein: 1,
calories: 1,
carbs: 1,
date: '2026-06-27',
});

const updated = await store.update(entry.id, {
name: 'A (revised)',
quantity: 2,
protein: 5,
calories: 50,
carbs: 10,
date: '2026-06-27',
});

expect(updated.id).toBe(entry.id);
expect(updated.createdAt).toBe(entry.createdAt);
expect(updated.name).toBe('A (revised)');
expect(updated.quantity).toBe(2);

const [stored] = await store.list();
expect(stored).toEqual(updated);
});

it('update() throws for an unknown id', async () => {
await expect(
store.update('missing-id', {
name: 'A',
quantity: 1,
protein: 1,
calories: 1,
carbs: 1,
date: '2026-06-27',
}),
).rejects.toThrow();
});

it('remove() deletes a food entry', async () => {
const entry = await store.create({
name: 'A',
Expand Down
19 changes: 19 additions & 0 deletions pwa/src/lib/foods/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ export function createIndexedDbFoodStore(): FoodStore {
return record;
}

async function update(id: string, entry: Omit<FoodEntry, 'id' | 'createdAt'>): Promise<FoodEntry> {
const db = await getDb();
const tx = db.transaction(FOODS_STORE, 'readwrite');
const store = tx.objectStore(FOODS_STORE);

const existing = await requestToPromise(store.get(id));
if (!existing) {
tx.abort();
throw new Error(`Food entry not found: ${id}`);
}

const updated: FoodEntry = { ...entry, id: existing.id, createdAt: existing.createdAt };
store.put(updated);
await transactionDone(tx);

return updated;
}

async function remove(id: string): Promise<void> {
const db = await getDb();
const tx = db.transaction(FOODS_STORE, 'readwrite');
Expand All @@ -74,6 +92,7 @@ export function createIndexedDbFoodStore(): FoodStore {
list,
listByDate,
create,
update,
remove,
listRecentDistinct,
};
Expand Down
1 change: 1 addition & 0 deletions pwa/src/lib/foods/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface FoodStore {
list(): Promise<FoodEntry[]>;
listByDate(date: string): Promise<FoodEntry[]>;
create(entry: Omit<FoodEntry, 'id' | 'createdAt'>): Promise<FoodEntry>;
update(id: string, entry: Omit<FoodEntry, 'id' | 'createdAt'>): Promise<FoodEntry>;
remove(id: string): Promise<void>;
/** Most recently logged entry for each distinct food name, newest first. */
listRecentDistinct(): Promise<FoodEntry[]>;
Expand Down
19 changes: 19 additions & 0 deletions pwa/src/routes/Food.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@
border-radius: 8px;
}

.cancelButton {
padding: 0.6rem 1rem;
font-size: 1rem;
font-weight: 600;
color: #007aff;
background: #fff;
border: 1px solid #d1d1d6;
border-radius: 8px;
}

.sectionTitle {
font-size: 0.85rem;
font-weight: 600;
Expand Down Expand Up @@ -149,11 +159,20 @@
border-radius: 8px;
}

.rowEditing {
background: #e5f1ff;
}

.rowMain {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.2rem;
padding: 0;
background: none;
border: none;
text-align: left;
}

.rowName {
Expand Down
44 changes: 38 additions & 6 deletions pwa/src/routes/Food.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function Food() {
const [recentFoods, setRecentFoods] = createSignal<FoodEntry[]>([]);
const [targets, setTargets] = createSignal(getFoodTargets());
const [form, setForm] = createSignal<FormState>(EMPTY_FORM);
const [editingId, setEditingId] = createSignal<string | undefined>();

const refresh = async () => {
const [today, recent] = await Promise.all([
Expand Down Expand Up @@ -54,6 +55,7 @@ function Food() {
};

const handleSelectRecent = (entry: FoodEntry) => {
setEditingId(undefined);
setForm({
name: entry.name,
protein: String(entry.protein),
Expand All @@ -63,26 +65,51 @@ function Food() {
});
};

const handleEdit = (entry: FoodEntry) => {
setEditingId(entry.id);
setForm({
name: entry.name,
protein: String(entry.protein),
calories: String(entry.calories),
carbs: String(entry.carbs),
quantity: String(entry.quantity),
});
};

const handleCancelEdit = () => {
setEditingId(undefined);
setForm(EMPTY_FORM);
};

const handleSubmit = async (event: Event) => {
event.preventDefault();
const current = form();
const name = current.name.trim();
if (!name) return;

await foodStore.create({
const record = {
name,
quantity: Number(current.quantity) || 1,
protein: Number(current.protein) || 0,
calories: Number(current.calories) || 0,
carbs: Number(current.carbs) || 0,
date: todayDateKey(),
});
};

const id = editingId();
if (id) {
await foodStore.update(id, record);
} else {
await foodStore.create(record);
}

setEditingId(undefined);
setForm(EMPTY_FORM);
await refresh();
};

const handleRemove = async (id: string) => {
if (editingId() === id) handleCancelEdit();
await foodStore.remove(id);
await refresh();
};
Expand Down Expand Up @@ -156,8 +183,13 @@ function Food() {
onInput={(event) => updateField('quantity', event.currentTarget.value)}
/>
<button type="submit" class={styles.addButton}>
Add
{editingId() ? 'Save Changes' : 'Add'}
</button>
<Show when={editingId()}>
<button type="button" class={styles.cancelButton} onClick={handleCancelEdit}>
Cancel
</button>
</Show>
</div>
</form>

Expand Down Expand Up @@ -187,8 +219,8 @@ function Food() {
<div class={styles.list}>
<For each={todayEntries()}>
{(entry) => (
<div class={styles.row}>
<div class={styles.rowMain}>
<div class={styles.row} classList={{ [styles.rowEditing]: editingId() === entry.id }}>
<button type="button" class={styles.rowMain} onClick={() => handleEdit(entry)}>
<div class={styles.rowName}>
{entry.name}
<Show when={entry.quantity !== 1}>
Expand All @@ -200,7 +232,7 @@ function Food() {
{Math.round(entry.calories * entry.quantity)} cal ·{' '}
{Math.round(entry.carbs * entry.quantity)}g carbs
</div>
</div>
</button>
<button
type="button"
class={styles.removeButton}
Expand Down
Loading