forked from mirror/crumbs-original
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2c545ac5 | ||
|
|
ba4f37d413 | ||
|
|
6f7560abd1 | ||
|
|
5e46545dc6 | ||
|
|
f459f39ae6 | ||
|
|
b71bdf63e6 | ||
|
|
14f7c1deaa | ||
|
|
56b0510789 | ||
|
|
bd9762600e | ||
|
|
f1ee1744e7 | ||
|
|
40e32ae8fc | ||
|
|
c1c4fe5056 | ||
|
|
e54e706b65 |
@@ -18,6 +18,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
tag: ${{ steps.release-tag.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -34,4 +36,62 @@ jobs:
|
||||
node-version: 22
|
||||
|
||||
- name: Run release script
|
||||
run: bash scripts/release.sh ${{ github.event.inputs.bump }}
|
||||
env:
|
||||
BUMP_TYPE: ${{ github.event.inputs.bump }}
|
||||
run: bash scripts/release.sh "$BUMP_TYPE"
|
||||
|
||||
- name: Output new tag
|
||||
id: release-tag
|
||||
run: echo "tag=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
ci:
|
||||
needs: bump
|
||||
uses: ./.github/workflows/ci.yml
|
||||
|
||||
release:
|
||||
name: Build & Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [bump, ci]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.bump.outputs.tag }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.bump.outputs.tag }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.bump.outputs.tag }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.bump.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -54,11 +54,3 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: true
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
steps:
|
||||
- name: Trigger Dokploy redeploy
|
||||
run: curl -X POST "${{ secrets.DOKPLOY_WEBHOOK_URL }}"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
crumbs:
|
||||
build: .
|
||||
image: ghcr.io/bretzel-app/crumbs:0.3.1
|
||||
container_name: crumbs
|
||||
ports:
|
||||
- "3000:3000"
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
- NODE_ENV=production
|
||||
- DATA_DIR=/data
|
||||
- DATABASE_URL=/data/crumbs.db
|
||||
- ORIGIN=http://localhost:3000
|
||||
- ORIGIN=${ORIGIN:-http://localhost:3000}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "crumbs",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
+2
-1
@@ -60,7 +60,8 @@ echo "Bumping $BUMP version..."
|
||||
npm version "$BUMP" -m "chore: bump version to %s" --no-git-tag-version
|
||||
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
git add package.json
|
||||
sed -i'' -e "s|image: ghcr.io/bretzel-app/crumbs:.*|image: ghcr.io/bretzel-app/crumbs:${NEW_VERSION}|" docker-compose.yml
|
||||
git add package.json docker-compose.yml
|
||||
git commit -m "chore: bump version to $NEW_VERSION"
|
||||
git tag "v$NEW_VERSION"
|
||||
|
||||
|
||||
+6
-4
@@ -6,10 +6,12 @@
|
||||
<meta name="theme-color" content="#f0e6d3" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥨</text></svg>"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="Crumbs" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<title>Crumbs</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -344,7 +344,7 @@
|
||||
data-testid="format-hr"
|
||||
>
|
||||
<Minus size={iconSize} />
|
||||
<span>Horizontal rule</span>
|
||||
<span>Divider</span>
|
||||
</button>
|
||||
<div class="my-1 h-px bg-[var(--border-subtle)]"></div>
|
||||
<button
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/favicon.svg" alt="" class="h-8 w-8" />
|
||||
<h1 class="font-['Press_Start_2P'] text-lg text-[var(--primary)]">Crumbs</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
{#if $notesLoaded && $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
|
||||
<div class="flex flex-col items-center justify-center py-20 text-[var(--text-muted)]">
|
||||
<span class="mb-4 text-7xl">🥨</span>
|
||||
<img src="/favicon.svg" alt="" class="mb-4 h-18 w-18" />
|
||||
<p class="font-['Press_Start_2P'] text-xs">
|
||||
{#if $currentFilter === 'trashed'}
|
||||
No crumbs in trash
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
|
||||
<div class="mb-6 text-center">
|
||||
<span class="text-5xl">🥨</span>
|
||||
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
|
||||
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Crumbs</h1>
|
||||
<p class="mt-2 text-sm text-[var(--text-muted)]">Enter your password to continue</p>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 shadow-[var(--card-shadow)]">
|
||||
<div class="mb-6 text-center">
|
||||
<span class="text-5xl">🥨</span>
|
||||
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
|
||||
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Welcome to Crumbs</h1>
|
||||
<p class="mt-2 text-sm text-[var(--text-muted)]">Set up your password to get started</p>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center bg-[var(--bg-base)]">
|
||||
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8 text-center">
|
||||
<span class="text-5xl">🥨</span>
|
||||
<img src="/favicon.svg" alt="Crumbs" class="mx-auto h-12 w-12" />
|
||||
<h1 class="mt-4 font-['Press_Start_2P'] text-xl text-[var(--primary)]">
|
||||
{page.status}
|
||||
</h1>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Crumbs by Bretzel",
|
||||
"short_name": "Crumbs",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#f9f4ea",
|
||||
"background_color": "#f9f4ea",
|
||||
"display": "standalone"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe('Checklist', () => {
|
||||
test('Scenario: Checklist UI replaces editor when checklist mode is enabled', async ({ authenticatedPage: page }) => {
|
||||
@@ -27,7 +27,7 @@ test.describe('Checklist', () => {
|
||||
await expect(page.getByText('Shopping List')).toBeVisible();
|
||||
|
||||
// When the user reopens the note
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Shopping List').click();
|
||||
|
||||
// Then the checklist is displayed with the saved item
|
||||
await expect(page.getByTestId('checklist')).toBeVisible();
|
||||
@@ -43,12 +43,12 @@ test.describe('Checklist', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user reopens the note and checks the item
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Tasks').click();
|
||||
await page.getByTestId('checklist-checkbox').first().check();
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// Then reopening the note shows the item is still checked
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Tasks').click();
|
||||
await expect(page.getByTestId('checklist-checkbox').first()).toBeChecked();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
import type { Page, Locator } from '@playwright/test';
|
||||
|
||||
/** Run a TipTap command chain via the exposed editor instance on the DOM element */
|
||||
@@ -94,7 +94,7 @@ test.describe('Rich text formatting', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user reopens the note
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Rich Text Note').click();
|
||||
|
||||
// Then the content is rendered as rich text in the TipTap editor
|
||||
await expect(page.getByTestId('tiptap-editor')).toBeVisible();
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { test as base, expect, type Page } from '@playwright/test';
|
||||
import { test as base, expect, type Page, type Locator } from '@playwright/test';
|
||||
|
||||
const TEST_PASSWORD = 'testpassword123';
|
||||
|
||||
/** Find a specific note card by its title text */
|
||||
export function noteCard(page: Page, title: string): Locator {
|
||||
return page.locator('[data-testid="note-card"]', { hasText: title });
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended test fixture that handles setup/auth.
|
||||
*/
|
||||
@@ -40,6 +45,10 @@ export async function setupAndLogin(page: Page) {
|
||||
await page.waitForURL('/');
|
||||
}
|
||||
// else: already on main page (session still valid)
|
||||
|
||||
// Wait for full hydration after auth redirect (window.location.href causes full reload).
|
||||
// networkidle ensures JS bundles have loaded and executed.
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe('Notes CRUD', () => {
|
||||
test('Scenario: Fresh account shows an empty notes list', async ({ authenticatedPage: page }) => {
|
||||
// Given the user has no notes
|
||||
// Then the empty state message is displayed
|
||||
await expect(page.getByText('No crumbs yet')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Scenario: New note appears in the notes list after creation', async ({ authenticatedPage: page }) => {
|
||||
// When the user creates a note titled "My First Note" with content "Hello world!"
|
||||
await page.getByTestId('new-note-btn').click();
|
||||
@@ -17,8 +11,7 @@ test.describe('Notes CRUD', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// Then the note is visible in the notes list
|
||||
await expect(page.getByTestId('note-card')).toBeVisible();
|
||||
await expect(page.getByText('My First Note')).toBeVisible();
|
||||
await expect(noteCard(page, 'My First Note')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Scenario: Edited note title is reflected in the notes list', async ({ authenticatedPage: page }) => {
|
||||
@@ -31,7 +24,7 @@ test.describe('Notes CRUD', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user changes the title to "Updated Title"
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Original Title').click();
|
||||
await page.getByTestId('note-title-input').clear();
|
||||
await page.getByTestId('note-title-input').fill('Updated Title');
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
@@ -47,8 +40,9 @@ test.describe('Notes CRUD', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user trashes the note
|
||||
await page.getByTestId('note-card').hover();
|
||||
await page.getByTestId('trash-btn').click();
|
||||
const card = noteCard(page, 'Delete Me');
|
||||
await card.hover();
|
||||
await card.getByTestId('trash-btn').click();
|
||||
|
||||
// Then the note is no longer visible
|
||||
await expect(page.getByText('Delete Me')).not.toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe('Offline Support', () => {
|
||||
test('Scenario: Sync status is visible to the user', async ({ authenticatedPage: page }) => {
|
||||
@@ -28,7 +28,7 @@ test.describe('Offline Support', () => {
|
||||
await page.context().setOffline(true);
|
||||
|
||||
// And the user edits the note title to "Offline Edited"
|
||||
await page.getByTestId('note-card').click();
|
||||
await noteCard(page, 'Offline Edit').click();
|
||||
await page.getByTestId('note-title-input').clear();
|
||||
await page.getByTestId('note-title-input').fill('Offline Edited');
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
@@ -107,8 +107,9 @@ test.describe('Offline Support', () => {
|
||||
await page.context().setOffline(true);
|
||||
|
||||
// And the user trashes the note
|
||||
await page.getByTestId('note-card').hover();
|
||||
await page.getByTestId('trash-btn').click();
|
||||
const card = noteCard(page, 'Offline Trash');
|
||||
await card.hover();
|
||||
await card.getByTestId('trash-btn').click();
|
||||
|
||||
// Then the note is no longer visible
|
||||
await expect(page.getByText('Offline Trash')).not.toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe('Organization Features', () => {
|
||||
test('Scenario: Pinned note appears under the Pinned section', async ({ authenticatedPage: page }) => {
|
||||
@@ -8,8 +8,9 @@ test.describe('Organization Features', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user pins the note
|
||||
await page.getByTestId('note-card').hover();
|
||||
await page.getByTestId('pin-btn').click();
|
||||
const pinCard = noteCard(page, 'Pin Me');
|
||||
await pinCard.hover();
|
||||
await pinCard.getByTestId('pin-btn').click();
|
||||
|
||||
// Then the "Pinned" section is visible
|
||||
await expect(page.getByText('Pinned')).toBeVisible();
|
||||
@@ -22,8 +23,9 @@ test.describe('Organization Features', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user archives the note
|
||||
await page.getByTestId('note-card').hover();
|
||||
await page.getByTestId('archive-btn').click();
|
||||
const archiveCard = noteCard(page, 'Archive Me');
|
||||
await archiveCard.hover();
|
||||
await archiveCard.getByTestId('archive-btn').click();
|
||||
|
||||
// Then the note is no longer visible in the main view
|
||||
await expect(page.getByText('Archive Me')).not.toBeVisible();
|
||||
@@ -40,7 +42,7 @@ test.describe('Organization Features', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// Then the note is saved and visible in the notes list
|
||||
await expect(page.getByTestId('note-card')).toBeVisible();
|
||||
await expect(noteCard(page, 'Colored Note')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Scenario: Filtering by tag shows only matching notes', async ({ authenticatedPage: page }) => {
|
||||
@@ -57,7 +59,7 @@ test.describe('Organization Features', () => {
|
||||
await page.getByTestId('close-editor-btn').click();
|
||||
|
||||
// When the user filters by the #important tag
|
||||
const tagChip = page.getByTestId('tag-chip').filter({ hasText: '#important' });
|
||||
const tagChip = page.getByTestId('tag-filter').getByTestId('tag-chip').filter({ hasText: '#important' });
|
||||
if (await tagChip.isVisible()) {
|
||||
await tagChip.click();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './helpers/fixtures.js';
|
||||
import { test, expect, noteCard } from './helpers/fixtures.js';
|
||||
|
||||
test.describe('Search', () => {
|
||||
test('Scenario: Searching by title returns the matching note', async ({ authenticatedPage: page }) => {
|
||||
@@ -42,6 +42,6 @@ test.describe('Search', () => {
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Then no notes are displayed
|
||||
await expect(page.getByTestId('note-card')).not.toBeVisible();
|
||||
await expect(page.getByTestId('note-card')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user