diff --git a/Makefile b/Makefile index 785a2c3..2b25aa4 100644 --- a/Makefile +++ b/Makefile @@ -53,16 +53,22 @@ test-e2e-ui: test-data-cleanup build npx playwright test --ui # Code quality commands +frontend-format: + cd frontend && $(MAKE) format + frontend-format-check: cd frontend && $(MAKE) format-check lint: golangci-lint run -fmt: frontend-format-check +format: frontend-format + go fmt ./... + +check-format: frontend-format-check go fmt ./... -quality: fmt lint test test-e2e +quality: check-format lint test test-e2e # Release commands for each platform release-darwin-arm64: diff --git a/db/db.go b/db/db.go index ffb4629..cc8ebe3 100644 --- a/db/db.go +++ b/db/db.go @@ -27,11 +27,11 @@ func New(cfg *config.Config) (*gorm.DB, error) { // Initialize default settings if they don't exist var settings models.Settings if err := db.FirstOrCreate(&settings, models.Settings{ - Title: "Captain", - Subtitle: "An AI authored blog engine", - ChromaStyle: "solarized-dark", - Theme: "default", - PostsPerPage: 10, + Title: "Captain", + Subtitle: "An AI authored blog engine", + ChromaStyle: "solarized-dark", + PostsPerPage: 10, + UseLogoAsFavicon: false, }).Error; err != nil { return nil, fmt.Errorf("failed to initialize default settings: %w", err) } diff --git a/e2e/specs/admin.spec.ts b/e2e/specs/admin.spec.ts index 7709313..69332bc 100644 --- a/e2e/specs/admin.spec.ts +++ b/e2e/specs/admin.spec.ts @@ -112,7 +112,7 @@ test.describe('Admin Panel E2E Tests', () => { const pageSlug = 'test-page'; await page.click('text=Pages'); - await page.click('text=Create New Page'); + await page.click('text=Create Your First Page'); await page.locator('input[name="title"]').pressSequentially(pageTitle); const slugValue = await page.locator('input[name="slug"]').inputValue(); expect(slugValue).toBe(pageSlug); @@ -137,7 +137,7 @@ test.describe('Admin Panel E2E Tests', () => { await page.click('text=Menu Items'); // Create custom URL menu item - await page.click('text=Create New Menu Item'); + await page.click('text=Create your first menu item!'); await page.fill('input[name="label"]', 'Custom Link'); await page.fill('input[name="url"]', 'https://example.com'); await page.click('button:has-text("Create Menu Item")'); @@ -181,18 +181,14 @@ test.describe('Admin Panel E2E Tests', () => { await page.fill('input[name="title"]', 'Updated Title'); await page.fill('input[name="subtitle"]', 'Updated Subtitle'); - await page.selectOption('select[name="theme"]', 'dark'); - await page.fill('input[name="posts_per_page"]', '1'); - - const settingsSaveResponse = page.waitForResponse('**/admin/settings'); - await page.click('button:has-text("Save Settings")'); + await page.fill('input[name="posts-per-page"]', '1'); - const response = await settingsSaveResponse; - await expect(response.status()).toBe(302); + await page.click('button:has-text("Save")'); + await expect(page.locator('button:has-text("Saved")')).toBeVisible(); // Verify post time updated await page.goto('/admin/posts'); - const thirdPost = page.locator('tr').nth(3); + const thirdPost = page.locator('tbody tr').nth(2); const publishedAt = await thirdPost.locator('td').nth(2).textContent(); expect(publishedAt).toContain('October 26, 1985 at 11:00 AM'); diff --git a/e2e/specs/post-visibility.spec.ts b/e2e/specs/post-visibility.spec.ts index ad57b1e..9f2230c 100644 --- a/e2e/specs/post-visibility.spec.ts +++ b/e2e/specs/post-visibility.spec.ts @@ -37,12 +37,9 @@ test.describe('Post Visibility', () => { // Go to settings page await page.goto('/admin/settings'); // Set page per page to 50 - await page.fill('input[name="posts_per_page"]', '50'); - const settingsSaveResponse = page.waitForResponse('**/admin/settings'); - await page.click('button:has-text("Save Settings")'); - - const response = await settingsSaveResponse; - await expect(response.status()).toBe(302); + await page.fill('input[name="posts-per-page"]', '50'); + await page.click('button:has-text("Save")'); + await expect(page.locator('button:has-text("Saved")')).toBeVisible(); // Verify posts are visible and properly marked when logged in await page.goto('/'); diff --git a/embedded/admin/static/css/admin.css b/embedded/admin/static/css/admin.css index 8540aee..6b86be6 100644 --- a/embedded/admin/static/css/admin.css +++ b/embedded/admin/static/css/admin.css @@ -316,37 +316,6 @@ code, pre { min-width: 200px; } -.btn { - display: inline-block; - padding: 0.5rem 1rem; - border-radius: 4px; - border: none; - cursor: pointer; - text-decoration: none; - font-size: 0.9rem; - line-height: 1.5; -} - -.btn-primary { - background: var(--admin-accent); - color: white; -} - -.btn-edit { - background: var(--admin-accent); - color: white; -} - -.btn-delete { - background: var(--admin-danger); - color: white; -} - -.btn-view { - background: var(--admin-success); - color: white; -} - .tag { display: inline-block; padding: 0.2rem 0.5rem; @@ -489,21 +458,6 @@ label { border: 1px solid var(--admin-border); } -.btn-submit { - background: var(--admin-accent); - color: #000; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - font-weight: bold; -} - -.btn-submit:hover { - opacity: 0.9; -} - .toggle-switch { position: relative; display: inline-block; diff --git a/embedded/admin/static/img/logo.png b/embedded/admin/static/img/logo.png new file mode 100644 index 0000000..40be35e Binary files /dev/null and b/embedded/admin/static/img/logo.png differ diff --git a/embedded/admin/static/js/admin.js b/embedded/admin/static/js/admin.js index 4e0c5b8..6158e43 100644 --- a/embedded/admin/static/js/admin.js +++ b/embedded/admin/static/js/admin.js @@ -76,6 +76,18 @@ function deleteUser(id) { }); } +function togglePassword(id) { + const input = document.getElementById(id); + const button = input.nextElementSibling; + if (input.type === 'password') { + input.type = 'text'; + button.textContent = 'Hide'; + } else { + input.type = 'password'; + button.textContent = 'Show'; + } +} + function initializeMenuItemForm() { const pageSelect = document.getElementById('page_id'); @@ -322,6 +334,47 @@ function openLogoMediaSelector() { }); } +function initializeDarkModeToggle() { + const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); + const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); + const themeToggleBtn = document.getElementById('theme-toggle'); + + // Change the icons inside the button based on previous settings + if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + themeToggleLightIcon.classList.remove('hidden'); + } else { + themeToggleDarkIcon.classList.remove('hidden'); + } + + themeToggleBtn.addEventListener('click', function() { + // toggle icons inside button + themeToggleDarkIcon.classList.toggle('hidden'); + themeToggleLightIcon.classList.toggle('hidden'); + + // if set via local storage previously + if (localStorage.getItem('color-theme')) { + if (localStorage.getItem('color-theme') === 'light') { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } + + // if NOT set via local storage previously + } else { + if (document.documentElement.classList.contains('dark')) { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } else { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } + } + + }); +} + (function () { document.querySelectorAll('[x-dynamic-date]').forEach((element) => { @@ -367,7 +420,7 @@ function openLogoMediaSelector() { } } else { error(json.error); - document.querySelector('.editor-container').scrollIntoView() + document.querySelector('.app-container').scrollIntoView() } } }); @@ -377,11 +430,12 @@ function openLogoMediaSelector() { let method = 'POST'; let url = '/admin/api/pages'; + done('saving'); + if (props.id) { method = 'PUT'; url = url + '/' + props.id; } - done('saving'); const resp = await fetch(url, { method, @@ -399,11 +453,69 @@ function openLogoMediaSelector() { } } else { error(json.error); - document.querySelector('.editor-container').scrollIntoView() + document.querySelector('.app-container').scrollIntoView() } } }); + Inity.register('settings', Apps.Settings, { + onSubmit: async (data, done, error, props) => { + let method = 'POST'; + let url = '/admin/api/settings'; + + if (props.id) { + method = 'PUT'; + } + + done('saving'); + + const resp = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + const json = await resp.json(); + + if (resp.ok) { + done('saved'); + } else { + error(json.error); + document.querySelector('.app-container').scrollIntoView() + } + }, + uploadLogoHandler: async (files, uploadStarted, uploadFinished) => { + const data = new FormData(); + data.append('logo', files[0]); + data.append('filename', files[0].name); + + const resp = await fetch('/admin/api/logo', { + method: 'POST', + body: data, + }); + + if (resp.ok) { + const json = await resp.json(); + uploadFinished(null, '/media/' + json.logoUrl); + } else { + uploadFinished(json.error, null); + } + uploadStarted(); + }, + deleteLogoHandler: async () => { + const resp = await fetch('/admin/api/logo', { + method: 'DELETE', + }); + + if (!resp.ok) { + const json = await resp.json(); + console.error(json.error); + } + }, + }); + document.addEventListener("DOMContentLoaded", () => Inity.attach()); })(); @@ -412,4 +524,5 @@ document.addEventListener('DOMContentLoaded', () => { initializeMenuItemForm(); initializeMenuItems(); initializeMenuToggle(); + initializeDarkModeToggle(); }); diff --git a/embedded/admin/static/js/menu.js b/embedded/admin/static/js/menu.js new file mode 100644 index 0000000..b3541b9 --- /dev/null +++ b/embedded/admin/static/js/menu.js @@ -0,0 +1,39 @@ +document.addEventListener('DOMContentLoaded', function() { + const menuToggle = document.getElementById('menu-toggle'); + const sidebar = document.getElementById('sidebar'); + + function toggleMenu() { + const isOpen = !sidebar.classList.contains('-translate-x-full'); + + if (isOpen) { + // Close menu + sidebar.classList.add('-translate-x-full'); + document.body.classList.remove('overflow-hidden'); + } else { + // Open menu + sidebar.classList.remove('-translate-x-full'); + document.body.classList.add('overflow-hidden'); + } + } + + menuToggle.addEventListener('click', toggleMenu); + + // Handle clicks outside menu on mobile + document.addEventListener('click', function(event) { + const isMobile = window.innerWidth < 768; + const isOpen = !sidebar.classList.contains('-translate-x-full'); + const clickedOutside = !sidebar.contains(event.target) && !menuToggle.contains(event.target); + + if (isMobile && isOpen && clickedOutside) { + toggleMenu(); + } + }); + + // Close menu when window is resized to desktop view + window.addEventListener('resize', function() { + if (window.innerWidth >= 768) { // md breakpoint + sidebar.classList.add('-translate-x-full'); + document.body.classList.remove('overflow-hidden'); + } + }); +}); diff --git a/embedded/admin/templates/admin_404.tmpl b/embedded/admin/templates/admin_404.tmpl index 85004b4..d3e6dae 100644 --- a/embedded/admin/templates/admin_404.tmpl +++ b/embedded/admin/templates/admin_404.tmpl @@ -4,8 +4,8 @@

404 - Page Not Found

The page you are looking for does not exist.

- - Back to Dashboard + + ← Back to Dashboard
diff --git a/embedded/admin/templates/admin_500.tmpl b/embedded/admin/templates/admin_500.tmpl index 0425d05..4a86d8a 100644 --- a/embedded/admin/templates/admin_500.tmpl +++ b/embedded/admin/templates/admin_500.tmpl @@ -8,8 +8,8 @@ {{end}}
- - Back to Dashboard + + ← Back to Dashboard
diff --git a/embedded/admin/templates/admin_confirm_delete_media.tmpl b/embedded/admin/templates/admin_confirm_delete_media.tmpl index 5ea24e2..8a15e51 100644 --- a/embedded/admin/templates/admin_confirm_delete_media.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_media.tmpl @@ -1,13 +1,11 @@ {{ template "admin_header" . }} -
-

Confirm Delete Media

-
-

Are you sure you want to delete "{{.media.Name}}"?

-

This action cannot be undone.

-
- - Cancel -
+

Confirm Delete Media

+
+

Are you sure you want to delete "{{.media.Name}}"?

+

This action cannot be undone.

+
+ + Cancel
{{ template "admin_footer" . }} diff --git a/embedded/admin/templates/admin_confirm_delete_menu_item.tmpl b/embedded/admin/templates/admin_confirm_delete_menu_item.tmpl index 0c18945..f4273dd 100644 --- a/embedded/admin/templates/admin_confirm_delete_menu_item.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_menu_item.tmpl @@ -1,13 +1,11 @@ {{ template "admin_header" . }} -
-

Confirm Delete Menu Item

-
-

Are you sure you want to delete the menu item "{{.menuItem.Label}}"?

-

This action cannot be undone.

-
- - Cancel -
+

Confirm Delete Menu Item

+
+

Are you sure you want to delete the menu item "{{.menuItem.Label}}"?

+

This action cannot be undone.

+
+ + Cancel
{{ template "admin_footer" . }} \ No newline at end of file diff --git a/embedded/admin/templates/admin_confirm_delete_page.tmpl b/embedded/admin/templates/admin_confirm_delete_page.tmpl index b7cd600..5a91bae 100644 --- a/embedded/admin/templates/admin_confirm_delete_page.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_page.tmpl @@ -1,13 +1,11 @@ {{ template "admin_header" . }} -
-

Confirm Delete Page

-
-

Are you sure you want to delete the page "{{.page.Title}}"?

-

This action cannot be undone.

-
- - Cancel -
+

Confirm Delete Page

+
+

Are you sure you want to delete the page "{{.page.Title}}"?

+

This action cannot be undone.

+
+ + Cancel
{{ template "admin_footer" . }} \ No newline at end of file diff --git a/embedded/admin/templates/admin_confirm_delete_post.tmpl b/embedded/admin/templates/admin_confirm_delete_post.tmpl index 39b2fcf..41a835c 100644 --- a/embedded/admin/templates/admin_confirm_delete_post.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_post.tmpl @@ -1,13 +1,11 @@ {{ template "admin_header" . }} -
-

Confirm Delete Post

-
-

Are you sure you want to delete the post "{{.post.Title}}"?

-

This action cannot be undone.

-
- - Cancel -
+

Confirm Post Deletion

+
+

Are you sure you want to delete the post "{{.post.Title}}"?

+

This action cannot be undone.

+
+ + Cancel
{{ template "admin_footer" . }} \ No newline at end of file diff --git a/embedded/admin/templates/admin_confirm_delete_tag.tmpl b/embedded/admin/templates/admin_confirm_delete_tag.tmpl index a55ecd3..c535cf5 100644 --- a/embedded/admin/templates/admin_confirm_delete_tag.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_tag.tmpl @@ -1,13 +1,11 @@ {{ template "admin_header" . }} -
-

Confirm Delete Tag

-
-

Are you sure you want to delete the tag "{{.tag.Name}}"?

-

This action cannot be undone.

-
- - Cancel -
+

Confirm Delete Tag

+
+

Are you sure you want to delete the tag "{{.tag.Name}}"?

+

This action cannot be undone.

+
+ + Cancel
{{ template "admin_footer" . }} \ No newline at end of file diff --git a/embedded/admin/templates/admin_confirm_delete_user.tmpl b/embedded/admin/templates/admin_confirm_delete_user.tmpl index 949da14..b3e55fe 100644 --- a/embedded/admin/templates/admin_confirm_delete_user.tmpl +++ b/embedded/admin/templates/admin_confirm_delete_user.tmpl @@ -1,48 +1,21 @@ {{ template "admin_header" . }} -
-