orionplace-nuxt 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -4
- package/bin/orionplace-nuxt.mjs +38 -7
- package/package.json +1 -1
- package/runtime/marketplace.mjs +12 -0
- package/src/install.mjs +5 -4
- package/src/project-readme.mjs +8 -3
- package/src/service.mjs +186 -0
- package/templates/app/components/orionplace/blog/BlogArticle.README.md +1 -1
- package/templates/app/components/orionplace/blog/BlogArticle.vue +78 -1
- package/templates/app/components/orionplace/legal/CookieNotice.README.md +34 -0
- package/templates/app/components/orionplace/legal/CookieNotice.vue +84 -0
- package/templates/app/components/orionplace/legal/StorefrontPrivacy.README.md +35 -0
- package/templates/app/components/orionplace/legal/StorefrontPrivacy.vue +124 -0
- package/templates/app/components/orionplace/products/ProductReviewImageViewer.README.md +3 -2
- package/templates/app/components/orionplace/products/ProductReviewImageViewer.vue +85 -11
- package/templates/app/components/orionplace/storefront/StorefrontFooter.vue +12 -0
- package/templates/app/components/orionplace/subscriptions/StorefrontTariff.vue +21 -1
- package/templates/app/components/orionplace/subscriptions/StorefrontTariffs.vue +23 -2
- package/templates/app/components/orionplace/tariffs/TariffCard.vue +13 -3
- package/templates/app/components/orionplace/tariffs/TariffCardPrices.README.md +1 -1
- package/templates/app/components/orionplace/tariffs/TariffCardPrices.vue +24 -4
- package/templates/app/composables/useOrionplaceSubscriptions.ts +43 -0
- package/templates/app/layouts/default.vue +7 -0
- package/templates/app/pages/privacy.vue +17 -0
- package/templates/app/stores/orionplaceCatalog.ts +20 -0
- package/templates/app/utils/orionplace/blogImageViewer.ts +84 -0
- package/templates/app/utils/orionplace/blogLabels.ts +4 -0
- package/templates/app/utils/orionplace/footerLabels.ts +2 -0
- package/templates/app/utils/orionplace/pageSeo.ts +10 -0
- package/templates/app/utils/orionplace/privacyLabels.ts +198 -0
- package/templates/app/utils/orionplace/reviewLabels.ts +4 -0
- package/templates/app/utils/orionplace/tariffLabels.ts +2 -0
- package/templates/app/utils/orionplace/tariffs.ts +49 -0
- package/templates/orionplace/COMPONENTS.md +13 -3
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ Framework imports such as `#imports` keep their own names.
|
|
|
129
129
|
<!-- components:start (generated by scripts/generate-package-readme-components.mjs) -->
|
|
130
130
|
## Components
|
|
131
131
|
|
|
132
|
-
The starter includes **
|
|
132
|
+
The starter includes **126 storefront components**, copied into
|
|
133
133
|
`app/components/orionplace/` of your project. Each one has a README in English and Russian
|
|
134
134
|
next to its source that describes its inputs, data and actions.
|
|
135
135
|
|
|
@@ -294,6 +294,8 @@ Journal content and customer contact flows.
|
|
|
294
294
|
| **Contact details** | `AboutPage` | Display the store’s published company and contact information. |
|
|
295
295
|
| **Contact form** | `ContactForm` | Sending a question to the store. |
|
|
296
296
|
| **Unsubscribe from emails** | `EmailUnsubscribe` | Confirming an unsubscribe request from an email link. |
|
|
297
|
+
| **Privacy policy page** | `StorefrontPrivacy` | The store’s personal data policy with a cookies section and the operator’s details. |
|
|
298
|
+
| **Cookie notice** | `CookieNotice` | A small notice about the necessary cookies with a link to the cookies section of the policy. |
|
|
297
299
|
|
|
298
300
|
### Shared interface
|
|
299
301
|
|
|
@@ -319,6 +321,8 @@ npx orionplace-nuxt add frontend www.shop.example.com
|
|
|
319
321
|
npx orionplace-nuxt remove frontend www.shop.example.com
|
|
320
322
|
npx orionplace-nuxt set mimic shop.example.com
|
|
321
323
|
sudo npx orionplace-nuxt install
|
|
324
|
+
npx orionplace-nuxt status
|
|
325
|
+
sudo npx orionplace-nuxt restart
|
|
322
326
|
npx orionplace-nuxt help
|
|
323
327
|
```
|
|
324
328
|
|
|
@@ -365,9 +369,34 @@ builds the project, runs it as the system service `orionplace-shop` on a free
|
|
|
365
369
|
local port and connects each storefront domain with nginx and a Let's Encrypt
|
|
366
370
|
certificate. Run it again after code or package updates; the service keeps its
|
|
367
371
|
port. Options: `--name <service>`, `--port <port>`, `--user <user>`,
|
|
368
|
-
`--no-domains`, `--yes`,
|
|
372
|
+
`--no-build` (install the build already in the folder), `--no-domains`, `--yes`,
|
|
373
|
+
`--dry-run`. Full guide:
|
|
369
374
|
[Install a storefront](https://orionmarket.place/en/sandbox/install/storefront).
|
|
370
375
|
|
|
376
|
+
### Manage the service
|
|
377
|
+
|
|
378
|
+
Run these in the project folder, or name the service from anywhere with
|
|
379
|
+
`--name shop`:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
npx orionplace-nuxt status # running?, autostart, port, home page answer, domains
|
|
383
|
+
sudo npx orionplace-nuxt start
|
|
384
|
+
sudo npx orionplace-nuxt stop
|
|
385
|
+
sudo npx orionplace-nuxt restart # waits until the home page answers again
|
|
386
|
+
sudo npx orionplace-nuxt logs 200 # last log lines; --follow keeps reading
|
|
387
|
+
npx orionplace-nuxt list # every storefront service on this server
|
|
388
|
+
sudo npx orionplace-nuxt uninstall # stop and remove the service (alias: cleanup)
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`start` and `restart` wait up to 90 seconds for the home page and print the
|
|
392
|
+
last log lines when it does not answer. `status` ends with code 3 when the
|
|
393
|
+
storefront is stopped or does not answer, so scripts and monitors can use it.
|
|
394
|
+
`uninstall` (or `cleanup`) stops the service, takes it out of autostart and
|
|
395
|
+
deletes its unit file and registration; the project folder and its build stay.
|
|
396
|
+
With the `orionplace` server tool it runs `orionplace app remove`, which also
|
|
397
|
+
turns the storefront's domains into a placeholder page. `--dry-run` shows what it
|
|
398
|
+
would remove; `--yes` skips the question.
|
|
399
|
+
|
|
371
400
|
## Generated files
|
|
372
401
|
|
|
373
402
|
| Path | Purpose |
|
|
@@ -430,6 +459,20 @@ authorization and cookies are forwarded. Backend statuses are retained and cooki
|
|
|
430
459
|
Domain attributes are removed to bind cookies to the storefront. Gateway responses
|
|
431
460
|
use `Cache-Control: no-store`. Network failures return `502`.
|
|
432
461
|
|
|
462
|
+
A changing request (POST, PUT, PATCH, DELETE) from another site is rejected with
|
|
463
|
+
`403`. A request from the page's own host or the storefront domain passes, and the
|
|
464
|
+
backend receives it with the storefront domain's origin
|
|
465
|
+
(`https://shop.example.com`), the one it accepts forms from. So a host that shows
|
|
466
|
+
the storefront through `X-Front-Domain`, such as a preview host or a local port,
|
|
467
|
+
can sign in too.
|
|
468
|
+
|
|
469
|
+
A changing request (POST, PUT, PATCH, DELETE) from another site is rejected with
|
|
470
|
+
`403`. A request from the page's own host or the storefront domain passes, and the
|
|
471
|
+
backend receives it with the storefront domain's origin
|
|
472
|
+
(`https://shop.example.com`), the one it accepts forms from. So a host that shows
|
|
473
|
+
the storefront through `X-Front-Domain`, such as a preview host or a local port,
|
|
474
|
+
can sign in too.
|
|
475
|
+
|
|
433
476
|
Browser requests use `/api/...`. For SSR, use Nuxt's `useRequestFetch()` to pass
|
|
434
477
|
the current request's domain and authentication to the same gateway. See the
|
|
435
478
|
project README for examples and cookie handling.
|
|
@@ -1055,10 +1098,14 @@ red `*` directly after the label text.
|
|
|
1055
1098
|
|
|
1056
1099
|
The default layout renders the [footer](https://orionmarket.place/en/sandbox/components/nuxt/footer)
|
|
1057
1100
|
with real store categories, home-section anchors, profile/journal navigation,
|
|
1058
|
-
a contact-page link and social links. On `/contact`, `StorefrontContact` places
|
|
1101
|
+
a contact-page link, a `/privacy` link and social links. On `/contact`, `StorefrontContact` places
|
|
1059
1102
|
company details (address, `tel:` phone link, registration numbers with copy
|
|
1060
1103
|
buttons and social links) in a column beside the bordered message-form panel;
|
|
1061
|
-
the columns stack on narrow screens and empty details are hidden.
|
|
1104
|
+
the columns stack on narrow screens and empty details are hidden. `/privacy`
|
|
1105
|
+
shows the store's privacy policy (`StorefrontPrivacy`, texts in
|
|
1106
|
+
`app/utils/orionplace/privacyLabels.ts`) with the operator's details; on the
|
|
1107
|
+
website the layout's `CookieNotice` links its `#cookies` section until the
|
|
1108
|
+
visitor dismisses it.
|
|
1062
1109
|
The home banner spans the available viewport
|
|
1063
1110
|
width without a frame. Its media area keeps the same proportions at every width; the image fills the
|
|
1064
1111
|
area without distortion and crops its edges when needed. Event media keeps its
|
package/bin/orionplace-nuxt.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { setupProject } from '../src/setup.mjs';
|
|
3
3
|
import { updateProject } from '../src/update.mjs';
|
|
4
4
|
import { installService } from '../src/install.mjs';
|
|
5
|
+
import { SERVICE_ACTIONS, SERVICE_ALIASES, controlService, findApp, printService } from '../src/service.mjs';
|
|
5
6
|
import { readFile } from 'node:fs/promises';
|
|
6
7
|
import { resolve } from 'node:path';
|
|
7
8
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -37,9 +38,20 @@ On your Linux server (run from the project root):
|
|
|
37
38
|
nginx and HTTPS. Run it again after changes or an update to rebuild and restart.
|
|
38
39
|
Server tool: https://orionmarket.place/downloads/orionplace.sh
|
|
39
40
|
|
|
41
|
+
Manage the installed service (from the project root, or name it with --name):
|
|
42
|
+
npx orionplace-nuxt status service, autostart, port and home page answer
|
|
43
|
+
sudo npx orionplace-nuxt start
|
|
44
|
+
sudo npx orionplace-nuxt stop
|
|
45
|
+
sudo npx orionplace-nuxt restart restart and wait until the home page answers
|
|
46
|
+
sudo npx orionplace-nuxt logs [lines] last log lines (100); --follow keeps reading
|
|
47
|
+
npx orionplace-nuxt list every storefront service on this server
|
|
48
|
+
sudo npx orionplace-nuxt uninstall stop the service and remove it (alias: cleanup);
|
|
49
|
+
the project folder stays
|
|
50
|
+
|
|
40
51
|
Options: --cwd <directory> --dry-run --help --version
|
|
41
52
|
Update option: --local (apply this CLI version instead of fetching the latest release)
|
|
42
|
-
Install options: --name <service> --port <port> --user <user> --no-domains --yes
|
|
53
|
+
Install options: --name <service> --port <port> --user <user> --no-build --no-domains --yes
|
|
54
|
+
Service options: --name <service> --follow (logs) --dry-run --yes (uninstall)
|
|
43
55
|
|
|
44
56
|
Update downloads the latest release, refreshes unedited source, and installs dependencies.
|
|
45
57
|
It preserves edited source, domain mappings and MIMIC. --dry-run leaves project files unchanged.
|
|
@@ -61,11 +73,14 @@ try {
|
|
|
61
73
|
let cwd = process.cwd();
|
|
62
74
|
let dryRun = false;
|
|
63
75
|
let local = false;
|
|
64
|
-
|
|
76
|
+
let follow = false;
|
|
77
|
+
const install = { domains: true, build: true, yes: false };
|
|
65
78
|
for (let index = 0; index < argv.length; index++) {
|
|
66
79
|
if (argv[index] === '--dry-run') dryRun = true;
|
|
67
80
|
else if (argv[index] === '--local') local = true;
|
|
68
81
|
else if (argv[index] === '--no-domains') install.domains = false;
|
|
82
|
+
else if (argv[index] === '--no-build') install.build = false;
|
|
83
|
+
else if (argv[index] === '--follow' || argv[index] === '-f') follow = true;
|
|
69
84
|
else if (argv[index] === '--yes' || argv[index] === '-y') install.yes = true;
|
|
70
85
|
else if (['--name', '--port', '--user'].includes(argv[index])) {
|
|
71
86
|
if (!argv[index + 1]) throw new Error(`${argv[index]} needs a value`);
|
|
@@ -76,11 +91,15 @@ try {
|
|
|
76
91
|
} else if (argv[index].startsWith('--')) throw new Error(`Unknown option: ${argv[index]}`);
|
|
77
92
|
else positional.push(argv[index]);
|
|
78
93
|
}
|
|
79
|
-
const [
|
|
94
|
+
const [given, target] = positional;
|
|
95
|
+
const command = SERVICE_ALIASES[given] || given;
|
|
80
96
|
if (local && command !== 'update') throw new Error('--local is only supported by update.');
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
const service = SERVICE_ACTIONS.includes(command);
|
|
98
|
+
if (command !== 'install' && (install.port || install.user || !install.domains || !install.build || (install.yes && command !== 'uninstall') || (install.name && !service))) {
|
|
99
|
+
throw new Error('--port, --user, --no-build, --no-domains and --yes are install options (--yes also for uninstall); --name is for install and the service commands.');
|
|
83
100
|
}
|
|
101
|
+
if (follow && command !== 'logs') throw new Error('--follow is a logs option.');
|
|
102
|
+
if (dryRun && service && command !== 'uninstall') throw new Error('--dry-run is only for uninstall among the service commands; status shows the state without changes.');
|
|
84
103
|
let result;
|
|
85
104
|
if (command === 'install' && positional.length === 1) {
|
|
86
105
|
const done = await installService({ cwd, dryRun, ...install });
|
|
@@ -91,14 +110,26 @@ try {
|
|
|
91
110
|
} else {
|
|
92
111
|
console.log(`\nInstalled orionplace-${done.name} (port ${done.port}, user ${done.user}).`);
|
|
93
112
|
for (const domain of done.frontends) {
|
|
94
|
-
if (!
|
|
113
|
+
if (!install.domains) console.log(` - ${domain}: not connected (--no-domains)`);
|
|
114
|
+
else if (!done.connected) console.log(` - ${domain}: not connected to nginx (see above)`);
|
|
95
115
|
else if (done.failed.includes(domain)) console.log(` ✗ ${domain}: not connected (see above)`);
|
|
96
116
|
else console.log(` ✓ https://${domain}`);
|
|
97
117
|
}
|
|
98
118
|
if (done.failed.length) console.log('Fix the problems above, then run: sudo orionplace domain add <domain>');
|
|
99
|
-
console.log(`Manage it
|
|
119
|
+
console.log(`Manage it from this folder: npx orionplace-nuxt status | sudo npx orionplace-nuxt restart | sudo npx orionplace-nuxt logs`);
|
|
100
120
|
console.log('After code changes or npx orionplace-nuxt update, run sudo npx orionplace-nuxt install again.');
|
|
101
121
|
}
|
|
122
|
+
} else if (service && positional.length <= (command === 'logs' ? 2 : 1)) {
|
|
123
|
+
if (command === 'uninstall' && !dryRun && !install.yes && process.stdin.isTTY) {
|
|
124
|
+
const app = await findApp({ cwd, name: install.name });
|
|
125
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
let answer;
|
|
127
|
+
try { answer = await prompt.question(`Stop and remove the service orionplace-${app.NAME}? The project stays in ${app.DIR}. [y/N] `); }
|
|
128
|
+
finally { prompt.close(); }
|
|
129
|
+
if (!/^y(es)?$/i.test(answer.trim())) { console.log('Nothing changed.'); process.exit(0); }
|
|
130
|
+
}
|
|
131
|
+
const done = await controlService({ action: command, cwd, name: install.name, lines: positional[1] ?? 100, follow, dryRun });
|
|
132
|
+
process.exitCode = printService(done);
|
|
102
133
|
} else if (command === 'init' && positional.length <= 2) {
|
|
103
134
|
result = await setupProject({ cwd: target ? resolve(cwd, target) : cwd, dryRun });
|
|
104
135
|
} else if (command === 'update' && positional.length === 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Nuxt 4 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and Russian, plus an Android app build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/runtime/marketplace.mjs
CHANGED
|
@@ -208,6 +208,18 @@ export async function forwardMarketplace(request, configuration = {}) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
headers.set('X-Front-Domain', tenant.domain);
|
|
211
|
+
// The backend accepts forms from the storefront domain's own origin. A public
|
|
212
|
+
// host that shows this storefront (MIMIC, a preview host or a local port)
|
|
213
|
+
// passed the same-origin check above, so it is sent as that origin.
|
|
214
|
+
let ownOrigin = false;
|
|
215
|
+
try {
|
|
216
|
+
ownOrigin = Boolean(origin) && originHosts.includes(new URL(origin).host);
|
|
217
|
+
} catch {
|
|
218
|
+
ownOrigin = false;
|
|
219
|
+
}
|
|
220
|
+
if (ownOrigin) {
|
|
221
|
+
headers.set('origin', `https://${tenant.domain}`);
|
|
222
|
+
}
|
|
211
223
|
headers.set('Accept-Encoding', 'identity');
|
|
212
224
|
const clientIp =
|
|
213
225
|
request.headers.get('x-init-ip') ||
|
package/src/install.mjs
CHANGED
|
@@ -222,7 +222,7 @@ async function ensureDomains(root, { yes }) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// The storefront's home page as the public domain sees it -> HTTP status, 0 when down.
|
|
225
|
-
function homeStatus(port, host) {
|
|
225
|
+
export function homeStatus(port, host) {
|
|
226
226
|
return new Promise(done => {
|
|
227
227
|
const call = request({ host: '127.0.0.1', port, path: '/', headers: { host }, timeout: 15000 }, response => {
|
|
228
228
|
response.resume();
|
|
@@ -234,7 +234,7 @@ function homeStatus(port, host) {
|
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
async function waitForHttp(port, host, seconds = 90) {
|
|
237
|
+
export async function waitForHttp(port, host, seconds = 90) {
|
|
238
238
|
const until = Date.now() + seconds * 1000;
|
|
239
239
|
while (Date.now() < until) {
|
|
240
240
|
const status = await homeStatus(port, host);
|
|
@@ -302,7 +302,8 @@ export async function installService({
|
|
|
302
302
|
return { dryRun, name: service, port: chosen, user: account.name, unitPath, unit, conf, frontends: settings.frontends || [], backend: settings.backend };
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
const rebuild = build || !existsSync(join(root, framework.output));
|
|
306
|
+
log(rebuild ? `\n==> Building the ${framework.title} storefront ${service}` : `\n==> Using the existing build of ${service} (--no-build)`);
|
|
306
307
|
// The website build; ORIONPLACE_APP_ORIGIN would make the Android app build.
|
|
307
308
|
const buildEnv = { ...process.env };
|
|
308
309
|
delete buildEnv.ORIONPLACE_APP_ORIGIN;
|
|
@@ -310,7 +311,7 @@ export async function installService({
|
|
|
310
311
|
const installed = await run('npm', ['install', '--no-audit', '--no-fund'], { cwd: root, env: buildEnv });
|
|
311
312
|
if (installed.code) throw new Error('npm install failed; see the messages above.');
|
|
312
313
|
}
|
|
313
|
-
if (
|
|
314
|
+
if (rebuild) {
|
|
314
315
|
const built = await run('npm', ['run', 'build'], { cwd: root, env: buildEnv });
|
|
315
316
|
if (built.code) throw new Error('The build failed; see the messages above. The running service (if any) was not changed.');
|
|
316
317
|
}
|
package/src/project-readme.mjs
CHANGED
|
@@ -122,9 +122,11 @@ red \`*\` directly after the label text.
|
|
|
122
122
|
|
|
123
123
|
The default layout renders the [footer](https://orionmarket.place/en/sandbox/components/nuxt/footer)
|
|
124
124
|
with real store categories, home-section anchors, profile/journal navigation,
|
|
125
|
-
a \`/contact\` link, social links and the company name in
|
|
126
|
-
Company contact and registration details appear beside the
|
|
127
|
-
\`/contact\`.
|
|
125
|
+
a \`/contact\` link, a \`/privacy\` link, social links and the company name in
|
|
126
|
+
its copyright line. Company contact and registration details appear beside the
|
|
127
|
+
message form on \`/contact\`. \`/privacy\` shows the store's privacy policy
|
|
128
|
+
(texts in \`app/utils/orionplace/privacyLabels.ts\`); on the website a cookie
|
|
129
|
+
notice links its \`#cookies\` section until the visitor dismisses it. The home banner spans the available viewport
|
|
128
130
|
width without a frame. Its media area keeps the same proportions at every width; the image fills the
|
|
129
131
|
area without distortion and crops its edges when needed. Event media keeps its
|
|
130
132
|
own reserved proportions, which differ on a narrow screen.
|
|
@@ -517,6 +519,9 @@ The quickest way: prepare the server with the OrionMarketplace server script,
|
|
|
517
519
|
then run \`sudo npx orionplace-nuxt install\` in this folder. It builds the
|
|
518
520
|
project, runs it as a system service and connects your domains with nginx and
|
|
519
521
|
HTTPS. Steps: [Install a storefront](https://orionmarket.place/en/sandbox/install/storefront).
|
|
522
|
+
Then manage it from this folder: \`npx orionplace-nuxt status\`, \`sudo npx orionplace-nuxt restart\`,
|
|
523
|
+
\`sudo npx orionplace-nuxt stop\`, \`sudo npx orionplace-nuxt start\` and \`sudo npx orionplace-nuxt logs\`.
|
|
524
|
+
\`sudo npx orionplace-nuxt uninstall\` (or \`cleanup\`) stops and removes the service; this folder stays.
|
|
520
525
|
|
|
521
526
|
To set the server up yourself instead:
|
|
522
527
|
|
package/src/service.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Manages the storefront service that "install" set up: start, stop, restart,
|
|
2
|
+
// status, logs and the list of every storefront service on this server.
|
|
3
|
+
// The service is found by its folder (the project root) or by --name.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { defaultPaths, framework, homeStatus, readApps, runCommand, waitForHttp } from './install.mjs';
|
|
8
|
+
|
|
9
|
+
export const SERVICE_ACTIONS = ['start', 'stop', 'restart', 'status', 'logs', 'list', 'uninstall'];
|
|
10
|
+
// "cleanup" is another name for uninstall.
|
|
11
|
+
export const SERVICE_ALIASES = { cleanup: 'uninstall' };
|
|
12
|
+
|
|
13
|
+
async function frontends(dir) {
|
|
14
|
+
try { return JSON.parse(await readFile(join(dir, 'orionplace/settings.json'), 'utf8')).frontends || []; }
|
|
15
|
+
catch { return []; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The registered storefront of this folder, or the one named with --name.
|
|
19
|
+
export async function findApp({ cwd = process.cwd(), name, paths = defaultPaths } = {}) {
|
|
20
|
+
const apps = await readApps(paths);
|
|
21
|
+
const root = resolve(cwd);
|
|
22
|
+
const app = name ? apps.find(item => item.NAME === name) : apps.find(item => item.DIR === root);
|
|
23
|
+
if (app) return app;
|
|
24
|
+
const known = apps.map(item => item.NAME).join(', ');
|
|
25
|
+
if (name) throw new Error(`No storefront service named ${name}.${known ? ` Installed: ${known}.` : ''}`);
|
|
26
|
+
throw new Error(`${root} is not installed as a service. Install it first: sudo npx orionplace-${framework.id} install`
|
|
27
|
+
+ (known ? `\nOr name another storefront: --name ${apps[0].NAME} (installed: ${known})` : ''));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// true / false; "unknown" when neither systemd nor OpenRC answers.
|
|
31
|
+
async function isRunning(service, { run, systemd }) {
|
|
32
|
+
const result = systemd
|
|
33
|
+
? await run('systemctl', ['is-active', '--quiet', service], { capture: true })
|
|
34
|
+
: await run('rc-service', [service, 'status'], { capture: true });
|
|
35
|
+
return result.code === 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function isEnabled(service, { run, systemd }) {
|
|
39
|
+
const result = systemd
|
|
40
|
+
? await run('systemctl', ['is-enabled', '--quiet', service], { capture: true })
|
|
41
|
+
: await run('rc-update', ['show', 'default'], { capture: true });
|
|
42
|
+
return systemd ? result.code === 0 : new RegExp(`^\\s*${service}\\s`, 'm').test(result.output);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function lastLogs(app, { run, systemd, paths, lines = 40 }) {
|
|
46
|
+
const result = systemd
|
|
47
|
+
? await run('journalctl', ['-u', app.SERVICE || `orionplace-${app.NAME}`, '-n', String(lines), '--no-pager'], { capture: true })
|
|
48
|
+
: await run('tail', ['-n', String(lines), join(paths.logs, `${app.NAME}.log`)], { capture: true });
|
|
49
|
+
return result.output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// One storefront's state: service, autostart, and the home page as its first domain sees it.
|
|
53
|
+
export async function serviceStatus(app, { run = runCommand, systemd = existsSync('/run/systemd/system'), probe = homeStatus } = {}) {
|
|
54
|
+
const service = app.SERVICE || `orionplace-${app.NAME}`;
|
|
55
|
+
const domains = await frontends(app.DIR);
|
|
56
|
+
const running = await isRunning(service, { run, systemd });
|
|
57
|
+
const enabled = await isEnabled(service, { run, systemd });
|
|
58
|
+
const http = running ? await probe(Number(app.PORT), domains[0] || 'localhost') : 0;
|
|
59
|
+
return { name: app.NAME, service, framework: app.FRAMEWORK, dir: app.DIR, port: Number(app.PORT), running, enabled, http, domains };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* controlService({ action, cwd, name, lines, follow }) runs one action and
|
|
64
|
+
* returns what happened; logs and follow print straight to the terminal.
|
|
65
|
+
*/
|
|
66
|
+
export async function controlService({
|
|
67
|
+
action, cwd = process.cwd(), name, lines = 100, follow = false,
|
|
68
|
+
paths = defaultPaths, run = runCommand, uid = process.getuid?.(),
|
|
69
|
+
systemd = existsSync('/run/systemd/system'), openrc = existsSync('/sbin/openrc-run'),
|
|
70
|
+
platform = process.platform, probe = homeStatus, wait = waitForHttp, dryRun = false,
|
|
71
|
+
} = {}) {
|
|
72
|
+
action = SERVICE_ALIASES[action] || action;
|
|
73
|
+
if (!SERVICE_ACTIONS.includes(action)) throw new Error(`Unknown service command ${action}. Use ${SERVICE_ACTIONS.join(', ')}.`);
|
|
74
|
+
if (platform !== 'linux') throw new Error(`${action} manages the Linux service that install set up. Run it on your server.`);
|
|
75
|
+
if (!systemd && !openrc) throw new Error('Neither systemd nor OpenRC was found on this server.');
|
|
76
|
+
|
|
77
|
+
if (action === 'list') {
|
|
78
|
+
const apps = await readApps(paths);
|
|
79
|
+
return { action, apps: await Promise.all(apps.map(app => serviceStatus(app, { run, systemd, probe }))) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const app = await findApp({ cwd, name, paths });
|
|
83
|
+
const service = app.SERVICE || `orionplace-${app.NAME}`;
|
|
84
|
+
if (action === 'status') return { action, ...await serviceStatus(app, { run, systemd, probe }) };
|
|
85
|
+
|
|
86
|
+
if (action === 'logs') {
|
|
87
|
+
const count = Number(lines);
|
|
88
|
+
if (!Number.isInteger(count) || count < 1) throw new Error('Give the number of log lines as a whole number, for example: logs 200');
|
|
89
|
+
const args = systemd
|
|
90
|
+
? ['journalctl', ['-u', service, '-n', String(count), '--no-pager', ...(follow ? ['--follow'] : [])]]
|
|
91
|
+
: ['tail', ['-n', String(count), ...(follow ? ['-F'] : []), join(paths.logs, `${app.NAME}.log`)]];
|
|
92
|
+
const result = await run(...args);
|
|
93
|
+
if (result.code) throw new Error(`${args[0]} could not read the logs of ${service}${uid !== 0 ? `; try: sudo npx orionplace-${framework.id} logs` : ''}.`);
|
|
94
|
+
return { action, name: app.NAME, service };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (action === 'uninstall') return uninstallService(app, { paths, run, uid, systemd, name, dryRun });
|
|
98
|
+
if (uid !== 0) throw new Error(`${action} needs root: sudo npx orionplace-${framework.id} ${action}${name ? ` --name ${name}` : ''}`);
|
|
99
|
+
const result = systemd
|
|
100
|
+
? await run('systemctl', [action, service], { capture: true })
|
|
101
|
+
: await run('rc-service', [service, action], { capture: true });
|
|
102
|
+
if (result.code) throw new Error(`Could not ${action} ${service}:\n${result.output.trim()}`);
|
|
103
|
+
if (action === 'stop') return { action, name: app.NAME, service, running: false };
|
|
104
|
+
|
|
105
|
+
const domains = await frontends(app.DIR);
|
|
106
|
+
const http = await wait(Number(app.PORT), domains[0] || 'localhost', 90);
|
|
107
|
+
if (!http) {
|
|
108
|
+
throw new Error(`${service} did not answer on port ${app.PORT} within 90 seconds. Last log lines:\n${await lastLogs(app, { run, systemd, paths })}`);
|
|
109
|
+
}
|
|
110
|
+
return { action, name: app.NAME, service, port: Number(app.PORT), running: true, http, domains };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stops the service, removes it from autostart, deletes its unit or init
|
|
115
|
+
* script and its registration. The project folder and its build stay. With
|
|
116
|
+
* the orionplace server tool, "orionplace app remove" does it and also turns
|
|
117
|
+
* the storefront's domains into a placeholder page.
|
|
118
|
+
*/
|
|
119
|
+
async function uninstallService(app, { paths, run, uid, systemd, name, dryRun }) {
|
|
120
|
+
const service = app.SERVICE || `orionplace-${app.NAME}`;
|
|
121
|
+
const unitPath = systemd ? join(paths.systemd, `${service}.service`) : join(paths.openrc, service);
|
|
122
|
+
const confPath = join(paths.etc, 'apps', `${app.NAME}.conf`);
|
|
123
|
+
const tool = (paths.tool || []).find(path => existsSync(path));
|
|
124
|
+
const result = { action: 'uninstall', name: app.NAME, service, dir: app.DIR, domains: await frontends(app.DIR), tool: Boolean(tool), unitPath, confPath, dryRun };
|
|
125
|
+
if (dryRun) return result;
|
|
126
|
+
if (uid !== 0) throw new Error(`uninstall needs root: sudo npx orionplace-${framework.id} uninstall${name ? ` --name ${name}` : ''}`);
|
|
127
|
+
if (tool) {
|
|
128
|
+
const removed = await run(tool, ['app', 'remove', app.NAME, '--yes']);
|
|
129
|
+
if (removed.code) throw new Error(`orionplace app remove ${app.NAME} failed; see the messages above.`);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
// A service that is already stopped or missing is fine: the goal is that it is gone.
|
|
133
|
+
if (systemd) await run('systemctl', ['disable', '--now', service], { capture: true });
|
|
134
|
+
else {
|
|
135
|
+
await run('rc-service', [service, 'stop'], { capture: true });
|
|
136
|
+
await run('rc-update', ['del', service, 'default'], { capture: true });
|
|
137
|
+
}
|
|
138
|
+
await rm(unitPath, { force: true });
|
|
139
|
+
if (systemd) await run('systemctl', ['daemon-reload'], { capture: true });
|
|
140
|
+
await rm(confPath, { force: true });
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The CLI output of controlService; returns the process exit code.
|
|
145
|
+
export function printService(result, log = console.log) {
|
|
146
|
+
const line = item => {
|
|
147
|
+
const state = item.running ? (item.http ? `running, home page HTTP ${item.http}` : 'running, home page does not answer') : 'stopped';
|
|
148
|
+
return `${item.name.padEnd(16)} ${String(item.framework || '').padEnd(7)} ${String(item.port).padEnd(6)} ${state}`;
|
|
149
|
+
};
|
|
150
|
+
if (result.action === 'list') {
|
|
151
|
+
if (!result.apps.length) {
|
|
152
|
+
log(`No storefront services on this server. Install one from its folder: sudo npx orionplace-${framework.id} install`);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
log(`${'NAME'.padEnd(16)} ${'TYPE'.padEnd(7)} ${'PORT'.padEnd(6)} STATE`);
|
|
156
|
+
for (const item of result.apps) log(line(item));
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
if (result.action === 'status') {
|
|
160
|
+
log(`${result.service} (${result.framework || framework.id}) in ${result.dir}`);
|
|
161
|
+
log(` Service: ${result.running ? 'running' : 'stopped'}${result.enabled ? ', starts with the server' : ', does not start with the server'}`);
|
|
162
|
+
log(` Port: 127.0.0.1:${result.port}`);
|
|
163
|
+
log(` Home page: ${result.running ? (result.http ? `HTTP ${result.http}` : 'does not answer') : '-'}`);
|
|
164
|
+
for (const domain of result.domains) log(` Domain: https://${domain}`);
|
|
165
|
+
return result.running && result.http && result.http < 500 ? 0 : 3;
|
|
166
|
+
}
|
|
167
|
+
if (result.action === 'uninstall') {
|
|
168
|
+
if (result.dryRun) {
|
|
169
|
+
log(`Would stop and remove ${result.service}:`);
|
|
170
|
+
log(result.tool ? ` orionplace app remove ${result.name} --yes (also turns its domains into a placeholder)` : ` ${result.unitPath}\n ${result.confPath}`);
|
|
171
|
+
log(`The project stays in ${result.dir}.`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
log(`Removed the service ${result.service}. The project and its build stay in ${result.dir}; delete the folder when you no longer need it.`);
|
|
175
|
+
if (!result.tool && result.domains.length) {
|
|
176
|
+
log(`nginx still sends ${result.domains.join(', ')} to the stopped service; remove those sites from your nginx configuration.`);
|
|
177
|
+
}
|
|
178
|
+
log(`Install it again with: sudo npx orionplace-${framework.id} install`);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (result.action === 'stop') log(`Stopped ${result.service}.`);
|
|
182
|
+
else if (result.action === 'start' || result.action === 'restart') {
|
|
183
|
+
log(`${result.action === 'start' ? 'Started' : 'Restarted'} ${result.service}: home page HTTP ${result.http} on 127.0.0.1:${result.port}.`);
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
@@ -20,7 +20,7 @@ After client-side navigation, while the first `blog_posts_get` request runs, a p
|
|
|
20
20
|
|
|
21
21
|
When `has_full_access` is false, the article shows its preview and an access panel: the text explains that the full article opens with an active subscription plan that includes it, and the Plans link opens the storefront plans page `/tariffs` with the current `locale`. `premium_tariff_ids` lists the IDs of the plans that include the article; the link passes them as `ids`, so the plans page shows only those plans. Without those IDs the link opens the whole list.
|
|
22
22
|
|
|
23
|
-
The first preview image is shown above the text, in an area that keeps its proportions. A post without a preview image, or with an image that fails to load, shows no cover. Article videos are shown with their poster image; their addresses may be backend paths (`/ps3/...`). In an Android app build the backend's own media paths (`/ps3/...`: cover, article videos, images and videos inside the text) load from the storefront address.
|
|
23
|
+
The first preview image is shown above the text, in an area that keeps its proportions. A post without a preview image, or with an image that fails to load, shows no cover. The cover and the images of the text open larger: a click, `Enter` or `Space` on one opens `ProductReviewImageViewer` at that image, with the article's other images a step away and each image's caption (its `figcaption`, else its `title`, else its `alt`) under it. The images get `role="button"`, a keyboard stop and an «Open image» name; a zoom-in cursor marks them. Images inside a link, a button or a product placement keep their own action. The helper is `app/utils/orionplace/blogImageViewer.ts`. Article videos are shown with their poster image; their addresses may be backend paths (`/ps3/...`). In an Android app build the backend's own media paths (`/ps3/...`: cover, article videos, images and videos inside the text) load from the storefront address.
|
|
24
24
|
|
|
25
25
|
When the first successful `blog_posts_get` response has no article with this ID, the server render of the supplied page answers HTTP 404; after client-side navigation the component emits `missing`. The backend returns an empty list for an unknown ID, an unpublished article and a subscription-only article that the current visitor cannot open. An article returned with `has_full_access = false` keeps its preview and the access panel with the Plans link. An article that disappears after sign-out keeps the “This article is unavailable” state on the page. A failed request is not a missing article: the error notice keeps its Refresh button. The supplied `app/pages/journal/[id].vue` opens the storefront 404 page on `missing` through `useOrionplaceMissingPage()`.
|
|
26
26
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref, watch } from 'vue';
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
|
3
3
|
import { useRuntimeConfig } from '#imports';
|
|
4
4
|
import { useOrionplaceStore } from '@/stores/orionplace';
|
|
5
5
|
import { useOrionplaceProductsStore } from '@/stores/orionplaceProducts';
|
|
@@ -25,6 +25,16 @@
|
|
|
25
25
|
nativeAppOrigin,
|
|
26
26
|
} from '@/utils/orionplace/nativeApp';
|
|
27
27
|
import { requestBlogPost } from '@/utils/orionplace/blogLayout';
|
|
28
|
+
import {
|
|
29
|
+
blogViewerImage,
|
|
30
|
+
blogViewerImages,
|
|
31
|
+
blogViewerPhotos,
|
|
32
|
+
blogViewerIndex,
|
|
33
|
+
isBlogViewerOpenKey,
|
|
34
|
+
markBlogViewerImages,
|
|
35
|
+
} from '@/utils/orionplace/blogImageViewer';
|
|
36
|
+
import type { IBlogViewerImage } from '@/utils/orionplace/blogImageViewer';
|
|
37
|
+
import ProductReviewImageViewer from '@/components/orionplace/products/ProductReviewImageViewer.vue';
|
|
28
38
|
import type { IBlogPost } from '@/interfaces/orionplace/blog/IBlogApi';
|
|
29
39
|
import type { IProductSecondaryVariantRequest } from '@/interfaces/orionplace/products/IProductSecondaryVariantRequest';
|
|
30
40
|
import type {
|
|
@@ -75,6 +85,10 @@
|
|
|
75
85
|
const task = useOrionplaceTask();
|
|
76
86
|
const post = ref<IBlogPost>();
|
|
77
87
|
const article = ref<HTMLElement>();
|
|
88
|
+
const coverImage = ref<HTMLImageElement>();
|
|
89
|
+
// The cover and the text images open larger in the viewer.
|
|
90
|
+
const viewerImages = ref<IBlogViewerImage[]>([]);
|
|
91
|
+
const viewerStart = ref(-1);
|
|
78
92
|
const accessChanged = ref(false);
|
|
79
93
|
const coverFailed = ref(false);
|
|
80
94
|
const appOrigin = nativeAppOrigin(
|
|
@@ -230,6 +244,44 @@
|
|
|
230
244
|
wasShown.value = true;
|
|
231
245
|
showProducts(props.initialPost.related_products);
|
|
232
246
|
}
|
|
247
|
+
function openableImages() {
|
|
248
|
+
return blogViewerImages([coverImage.value, article.value]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The text is set with v-html: its images become buttons after each render.
|
|
252
|
+
watch(
|
|
253
|
+
[html, cover, mounted, () => labels.value.imageOpen, () => post.value?.id],
|
|
254
|
+
() => {
|
|
255
|
+
void nextTick(() => markBlogViewerImages(openableImages(), labels.value.imageOpen));
|
|
256
|
+
},
|
|
257
|
+
{ flush: 'post' },
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
function openViewer(event: Event) {
|
|
261
|
+
const images = openableImages();
|
|
262
|
+
const index = blogViewerIndex(event.target, images);
|
|
263
|
+
if (index < 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
viewerImages.value = images.map((image) => {
|
|
268
|
+
const shown = blogViewerImage(image);
|
|
269
|
+
return image === coverImage.value ? { ...shown, alt: shown.alt || title.value } : shown;
|
|
270
|
+
});
|
|
271
|
+
viewerStart.value = index;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function onArticleKeydown(event: KeyboardEvent) {
|
|
275
|
+
if (isBlogViewerOpenKey(event)) {
|
|
276
|
+
openViewer(event);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// The viewer gives the focus back to the image that opened it.
|
|
281
|
+
function closeViewer() {
|
|
282
|
+
viewerStart.value = -1;
|
|
283
|
+
}
|
|
284
|
+
|
|
233
285
|
onMounted(() => {
|
|
234
286
|
mounted.value = true;
|
|
235
287
|
if (!preloaded) {
|
|
@@ -277,6 +329,8 @@
|
|
|
277
329
|
<article
|
|
278
330
|
v-if="post"
|
|
279
331
|
class="op-feature-stack mx-auto w-full max-w-[720px]"
|
|
332
|
+
@click="openViewer"
|
|
333
|
+
@keydown="onArticleKeydown"
|
|
280
334
|
>
|
|
281
335
|
<header class="op-feature-stack">
|
|
282
336
|
<h1
|
|
@@ -302,6 +356,7 @@
|
|
|
302
356
|
</div>
|
|
303
357
|
<img
|
|
304
358
|
v-if="cover && !coverFailed"
|
|
359
|
+
ref="coverImage"
|
|
305
360
|
:src="cover"
|
|
306
361
|
alt=""
|
|
307
362
|
width="3"
|
|
@@ -471,6 +526,16 @@
|
|
|
471
526
|
kind="journal"
|
|
472
527
|
:message="labels.unavailable"
|
|
473
528
|
/>
|
|
529
|
+
<ProductReviewImageViewer
|
|
530
|
+
v-if="viewerStart >= 0 && viewerImages.length"
|
|
531
|
+
:images="blogViewerPhotos(viewerImages)"
|
|
532
|
+
:index="viewerStart"
|
|
533
|
+
:label="labels.imageViewer"
|
|
534
|
+
:alt="title || labels.imageViewer"
|
|
535
|
+
:captions="viewerImages.map((image) => image.caption || image.alt)"
|
|
536
|
+
wide
|
|
537
|
+
@close="closeViewer"
|
|
538
|
+
/>
|
|
474
539
|
</section>
|
|
475
540
|
</template>
|
|
476
541
|
|
|
@@ -683,6 +748,18 @@
|
|
|
683
748
|
vertical-align: bottom;
|
|
684
749
|
}
|
|
685
750
|
|
|
751
|
+
/* Images that open in the viewer. */
|
|
752
|
+
[data-blog-article-cover][data-blog-image-viewer],
|
|
753
|
+
.op-feature-prose :deep(img[data-blog-image-viewer]) {
|
|
754
|
+
cursor: zoom-in;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
[data-blog-article-cover][data-blog-image-viewer]:focus-visible,
|
|
758
|
+
.op-feature-prose :deep(img[data-blog-image-viewer]:focus-visible) {
|
|
759
|
+
outline: 2px solid var(--orionplace-accent);
|
|
760
|
+
outline-offset: 3px;
|
|
761
|
+
}
|
|
762
|
+
|
|
686
763
|
.op-feature-prose :deep(:where(img, video)) {
|
|
687
764
|
display: block;
|
|
688
765
|
max-width: 100%;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CookieNotice · Nuxt
|
|
2
|
+
|
|
3
|
+
A small notice about the necessary cookies with a link to the cookies section of the policy.
|
|
4
|
+
|
|
5
|
+
The supplied layout shows it on every page of the website, not in the Android app build.
|
|
6
|
+
[English guide](https://orionmarket.place/en/sandbox/components/nuxt/cookie-notice) ·
|
|
7
|
+
[Russian guide](https://orionmarket.place/ru/sandbox/components/nuxt/cookie-notice)
|
|
8
|
+
|
|
9
|
+
## Contract and dependencies
|
|
10
|
+
|
|
11
|
+
After mounting, the notice appears at the bottom of the screen until the
|
|
12
|
+
visitor presses OK; the choice is kept in local storage under
|
|
13
|
+
`orionplace-cookie-notice-seen` (`COOKIE_NOTICE_STORAGE_KEY` in
|
|
14
|
+
`app/utils/orionplace/privacyLabels.ts`). Server HTML never contains it. The
|
|
15
|
+
storefront sets only necessary cookies, so the notice asks for no consent; a
|
|
16
|
+
store that adds analytics or advertising cookies needs a consent form instead.
|
|
17
|
+
|
|
18
|
+
`href` is the address of the cookies section with the language, for example
|
|
19
|
+
`/privacy?locale=ru#cookies`; the default is `/privacy#cookies`. An ordinary
|
|
20
|
+
click on the link emits `navigate` with `href`; the layout navigates.
|
|
21
|
+
|
|
22
|
+
Source is maintained in
|
|
23
|
+
`app/components/orionplace/legal/CookieNotice.vue`.
|
|
24
|
+
|
|
25
|
+
The configured Nuxt starter initializes Application Load and its named API client.
|
|
26
|
+
Component-source ZIPs contain imported children, types,
|
|
27
|
+
composables, labels and styles. The included `orionplace/COMPONENT-DOWNLOAD.md`
|
|
28
|
+
describes dependencies, theme and consumer configuration. Standalone file
|
|
29
|
+
copying requires the same dependencies. Links to storefront routes use the
|
|
30
|
+
pages supplied by the full starter.
|
|
31
|
+
|
|
32
|
+
## Endpoint coverage
|
|
33
|
+
|
|
34
|
+
None.
|