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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- "Request a feature" link in the admin sidebar footer — opens a pre-filled GitHub issue (enhancement label, a short title/body template) so store owners can suggest features without any API token or account setup on the store's side.
- Visual page builder with live preview. Every page (Admin → Pages) is built from **reorderable sections** with a **live preview** pane beside the editor — an iframe of your real storefront that re-renders as you add/reorder sections, change colours or pick a collection, with **Desktop/Mobile** width toggles and manual refresh. The editor is a two-pane layout — a collapsible left panel holds the page title, SEO, publish controls **and** the sections; the preview fills the right-hand side and can be expanded to full width. Sections **reorder by drag-and-drop** (or keyboard up/down buttons).
- **Product & collection page templates**: shared **Product page** and **Collection page** templates (Admin → Page templates) render builder sections on every product/collection page below the core content, **plus** each individual product and collection can add its own sections ("Edit page sections" on the product/collection editor) — the global template renders first, then the item's own.
- **Reusable sections** (Admin → Reusable sections): build a set of sections once and drop it onto any page with the **Reusable block** section — edit the block and every page using it updates. Blocks are edited in the same builder (with live preview); a missing/deleted block simply renders nothing.
Expand Down
5 changes: 5 additions & 0 deletions admin/partials/layout.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
{{admin.name}}
</a>
<a href="{{featureRequestUrl}}" target="_blank" rel="noopener"
class="sidebar-link" style="font-size:0.8rem;opacity:0.75;">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 21h6M10 17h4a5 5 0 10-4 0zM12 3v1M4.2 6.2l.7.7M2 13h1M21 13h1M19.1 6.9l-.7.7"/></svg>
Request a feature
</a>
{{#if cloudMode}}
<a href="https://cloud.squaark.com" target="_blank" rel="noopener"
class="sidebar-link" style="font-size:0.75rem;opacity:0.65;">
Expand Down
13 changes: 13 additions & 0 deletions src/admin/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import config from '../config';

const ADMIN_VIEWS = path.resolve(process.cwd(), 'admin');

// A deep link to GitHub's "new issue" form, pre-filled as a feature request.
// No API token to manage per store — the owner files it under their own GitHub
// account, so the link works the same for a self-hosted or a managed store.
const FEATURE_REQUEST_URL =
'https://github.com/Squaark/squaark/issues/new?labels=enhancement'
+ '&title=' + encodeURIComponent('Feature request: ')
+ '&body=' + encodeURIComponent(
'### What would you like to add or change?\n\n\n'
+ '### Why would it help your store?\n\n\n'
+ '---\nSent from the Squaark admin.',
);

const hbs = Handlebars.create();

function loadPartials() {
Expand Down Expand Up @@ -192,6 +204,7 @@ export async function render(template: string, context: Record<string, unknown>,
...context,
csrfToken,
cloudMode: config.cloudMode,
featureRequestUrl: FEATURE_REQUEST_URL,
update: context.update ?? getCachedUpdateStatus(),
};

Expand Down
31 changes: 31 additions & 0 deletions tests/admin/feature-request.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import { render } from '../../src/admin/render';

/** The admin layout footer carries a "Request a feature" link that deep-links to
* GitHub's new-issue form, pre-filled — no API token to manage per store. */
describe('feature-request link', () => {
const reply = { generateCsrf: async () => 'tok' } as never;

async function layoutHtml(): Promise<string> {
// Any template renders inside the shared layout; 404 needs the least context.
return render('404', { admin: { name: 'Jane', role: 'admin' }, settings: {} }, reply);
}

it('appears in the sidebar footer, opening the repo issue form in a new tab', async () => {
const html = await layoutHtml();
expect(html).toContain('Request a feature');
const m = html.match(/href="([^"]*issues\/new[^"]*)"[^>]*target="_blank"[^>]*rel="noopener"/);
expect(m).not.toBeNull();
expect(m![1]).toContain('github.com/Squaark/squaark/issues/new');
});

it('pre-fills the enhancement label, a title and a body once entities decode', async () => {
const html = await layoutHtml();
const raw = html.match(/href="([^"]*issues\/new[^"]*)"/)![1];
// Browsers decode HTML entities in the attribute before navigating.
const url = new URL(raw.replace(/&#x3D;/g, '=').replace(/&amp;/g, '&'));
expect(url.searchParams.get('labels')).toBe('enhancement');
expect(url.searchParams.get('title')).toBe('Feature request: ');
expect(url.searchParams.get('body')).toContain('What would you like to add or change?');
});
});
Loading