diff --git a/README.md b/README.md index 5c91169..247bc60 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,78 @@ -# create-svelte +# [Kkosmetickysalon.cz](https://kkosmetickysalon.cz) -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). +This is the repo of the website [Kkosmetickysalon.cz](https://kkosmetickysalon.cz), my mom's business, I also did the logos and branding, which are located in `/static` -## Creating a project +## Webapp stack -If you're seeing this, you've probably already done this step. Congrats! +### [Sveltekit with advanced config](https://kit.svelte.dev/docs/introduction) -```bash -# create a new project in the current directory -npm create svelte@latest +Sveltekit is simple, modern metaframework for svelte. The docs are great, love the decisions and features, don't need anything more or less for websites with moderate complexity and PWAs. -# create a new project in my-app -npm create svelte@latest my-app -``` +### [Skeleton UI library](https://skeleton.dev) -## Developing +This is perhaps the best UI library for sveltekit, their docs and their code is great -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +### Tailwind + PostCSS + Fontawesome local -```bash -npm run dev +Utility css is quick to develop, easy to remember, readable little boilerplate needed. Postcss as a dependency and for conditional css. Fontawesome free for local icons as fonts. -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` +### Most importantly, SCREW CMS, use .md or json or yaml and parse them -## Building +#### Motivation -To create a production version of your app: +I have spent considerable amount of time researching and learning the bloatware that is on the internet caused by the nocode movement and enough is enough. It takes a fraction of a time, if any to understand how json works, if you know it's a way to represent and structure content, then you're set. -```bash -npm run build -``` +- it's dead simple and you literally almost just import it +- You don't have to host strapi or a db on a 4GB RAM server in the cloud for making a post once a month +- Static makes better SEO -You can preview the production build with `npm run preview`. +#### Use case -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. +I have a $content path (`./src/content`), where I have `./services/` and `./posts` in the former I store individual service categories (and their respective items) in json files, in the latter I have posts, where I store .md files, which are subsequently processed by MDSveX and the html outputted from MDSveX is relayed to `./sluzby/[id]`. It's very DRY, there are no lockins and it's reusable on a lot of platforms. +Everything has a schema (e.g. `./sluzby/schema#`) or a type or a generatable template. +The schema can be used with a [JSON editor](https://github.com/json-editor/json-editor) to help nontechies write it. +The json data is then validated by [ajv](https://ajv.js.org) to match the type Service in `$lib/types/service.d.ts` + +## Features, Components and parts + +A list of mostly + +### [LibreMaps](https://svelte-maplibre.vercel.app/) + +Screw Google Maps, I knew I wanted to use OSM, maybe Mapbox, because I had experience with it, but this was a great ready made, quality solution, repo is [here](https://github.com/dimfeld/svelte-maplibre). + +### [Instagram feed](https://github.com/rodneylab/sveltekit-instagram-infinite-scroll) + +### CSP, custom hooks, custom headers + +Securing this app with the latest security features and web technologies. + +### Service Worker + +Why not? + +## Admin/DevOps Tools + +A list of mostly 3rd party useful tools this project uses. + +### Gitea action CI/CD workflow + +### hCaptcha + +For forms, I may remove this in favor of something else, because it could be a privacy and GDPR issue. Also screw Google and their reCaptcha. + +### Plausible self-hosted + +That or coding some metrics and using some opinionated solution myself. + +### Sentry - runtime prod & dev analysis + +Sentry is cool, I will probably not use 80% of their features, but when doing CSP and all sorts of reporting, this came in very handy. I don't really see an alternative with sveltekit. + +### Playwright - headless browser target testing + +TODO, probably sometime, It can be useful with the service posts. + +### Dockerfile + +Selfhosting this is the only way. I used ansible and terraform to get this thing in the air together with the analytics platform. It's on AWS for now, diff --git a/package.json b/package.json index 684e281..286914e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { - "name": "cosmeticstudio", + "name": "kkosmetickysalon", "version": "0.0.1", "private": true, + "type": "module", "scripts": { - "dev": "vite dev", - "build": "vite build", + "validate": "node ./tests/ValidateServices.js", + "dev": "pnpm run validate && vite dev --mode development", + "build": "pnpm run validate && vite build", + "build-dev": "pnpm run validate && vite build --mode development", "preview": "vite preview", - "test": "npm run test:integration && npm run test:unit", + "test": "pnpm run test:integration && npm run test:unit", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", @@ -22,7 +25,8 @@ "@fortawesome/fontawesome-free": "^6.5.1", "@sentry/sveltekit": "^7.107.0", "@sveltejs/adapter-node": "^5.0.1", - "@sveltejs/vite-plugin-svelte": "^3.0.2" + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "ajv": "^8.12.0" }, "devDependencies": { "@floating-ui/dom": "1.5.3", @@ -44,6 +48,7 @@ "prettier-plugin-svelte": "^2.10.1", "svelte": "^4.2.12", "svelte-check": "^3.6.7", + "svelte-maplibre": "^0.8.2", "svelte-preprocess": "^5.1.3", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", @@ -51,6 +56,5 @@ "vite": "^5.1.6", "vite-plugin-tailwind-purgecss": "^0.2.0", "vitest": "^0.32.4" - }, - "type": "module" + } } diff --git a/src/content/services/dalsi-sluzby.json b/src/content/services/dalsi-sluzby.json new file mode 100644 index 0000000..82b4962 --- /dev/null +++ b/src/content/services/dalsi-sluzby.json @@ -0,0 +1,34 @@ +{ + "$schema": "/src/routes/sluzby/schema.json", + "category": "DALŠÍ VELMI OBLÍBENÉ SLUŽBY", + "items": [ + { + "name": "Lifting řas booster (botox)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "lifting-ras-booster", + "price": 500, + "duration": 1 + }, + { + "name": "Laminace obočí + výživa", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "laminace-oboci-vyziva", + "price": 500, + "duration": 1 + }, + { + "name": "Úprava obočí (tvar + barva)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "uprava-oboci-tvar-barva", + "price": 250, + "duration": 1 + }, + { + "name": "Úprava obočí + řasy (tvar + barvení)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "uprava-oboci-rasy-tvar-barveni", + "price": 300, + "duration": 1 + } + ] +} diff --git a/src/content/services/depilace.json b/src/content/services/depilace.json new file mode 100644 index 0000000..feab20e --- /dev/null +++ b/src/content/services/depilace.json @@ -0,0 +1,69 @@ +{ + "$schema": "/src/routes/sluzby/schema.json", + "category": "Depilace", + "items": [ + { + "name": "Depilace Horní ret", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-horni-ret", + "price": 80, + "duration": 0.5 + }, + { + "name": "Depilace Brada", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-brada", + "price": 80, + "duration": 0.5 + }, + { + "name": "Depilace Obočí", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-oboci", + "price": 150, + "duration": 0.5 + }, + { + "name": "Depilace Tváře", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-tvare", + "price": 150, + "duration": 0.5 + }, + { + "name": "Depilace Podpaží", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-podpazi", + "price": 150, + "duration": 0.5 + }, + { + "name": "Depilace Předloktí", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-predlokti", + "price": 200, + "duration": 0.5 + }, + { + "name": "Depilace Celé ruce", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-cele-ruce", + "price": 350, + "duration": 1 + }, + { + "name": "Depilace Lýtka", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-lytka", + "price": 350, + "duration": 1 + }, + { + "name": "Depilace Celé nohy", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "depilace-cele-nohy", + "price": 500, + "duration": 1 + } + ] +} diff --git a/src/content/services/kosmeticke-osetreni.json b/src/content/services/kosmeticke-osetreni.json new file mode 100644 index 0000000..0517efe --- /dev/null +++ b/src/content/services/kosmeticke-osetreni.json @@ -0,0 +1,48 @@ +{ + "$schema": "/src/routes/sluzby/schema.json", + "category": "Kosmetické ošetření", + "items": [ + { + "name": "ZÁKLADNÍ CALM", + "description": "Diagnostika pleti, odlíčení tonizace, enzymatický peeling, kavitační peeling -ultarzvuková špachtle, séra dle typu pleti, masky (tvář,krk,dekolt), závěrečná péče (oční a denní krém)", + "id": "zakladni-calm", + "price": 500, + "duration": 1 + }, + { + "name": "ZÁKLADNÍ + CALM PLUS", + "description": "Diagnostika pleti, odlíčení tonizace, úprava obočí (vosk+pinzeta), barvení řas a obočí, depilace horní ret/brada, enzymatický peeling, kavitační peeling -ultarzvuková špachtle, séra dle typu pleti, masky (tvář,krk,dekolt), závěrečná péče (oční a denní krém)", + "id": "zakladni-calm-plus", + "price": 600, + "duration": 1 + }, + { + "name": "RELAXAČNÍ", + "description": "Diagnostika pleti, odlíčení tonizace, úprava obočí (vosk+pinzeta), barvení řas a obočí, depilace horní ret/brada, enzymatický peeling, kavitační peeling -ultarzvuková špachtle, séra, masáž relaxační (tvář,krk dekolt), masky (tvář,krk,dekolt), závěrečná péče (oční a denní krém)", + "id": "relaxacni", + "price": 690, + "duration": 1.5 + }, + { + "name": "LIFTINGOVÉ - ANTI AGE", + "description": "Diagnostika pleti, odlíčení tonizace, úprava obočí (vosk+pinzeta), barvení řas a obočí, depilace horní ret/brada, enzymatický peeling, kavitační peeling -ultarzvuková špachtle, vacupres ošetření – lifting obličeje krku a dekoltu, séra, masky (tvář,krk,dekolt), alginátová maska, závěrečná péče (oční a denní krém)", + "id": "liftingove-anti-age", + "price": 690, + "duration": 1.5 + }, + { + "name": "CLEAR + ANTI AKNÉ", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "clear-anti-akne", + "price": 690, + "duration": 1.5 + }, + { + "name": "Odlíčení + sérum + alginátová maska (PROJASNĚNÍ)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "odliceni-serum-alginatova-maska", + "price": 300, + "duration": 1 + } + ] +} diff --git a/src/content/services/permanent-mu.json b/src/content/services/permanent-mu.json new file mode 100644 index 0000000..754677f --- /dev/null +++ b/src/content/services/permanent-mu.json @@ -0,0 +1,69 @@ +{ + "$schema": "$routes/sluzby/schema.json", + "category": "Permanentní make-up", + "items": [ + { + "name": "Obočí Pudrové, Ombré", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "oboci", + "price": 3000, + "duration": 2.5 + }, + { + "name": "Horní linky - meziřasové přirozené", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "linky", + "price": 2000, + "duration": 2 + }, + { + "name": "Klasické linky - s ocáskem", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "classic-linky", + "price": 3000, + "duration": 2.5 + }, + { + "name": "Klasické linky - s ocáskem + spodní linky", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "classic-linky+spodni", + "price": 3500, + "duration": 2.5 + }, + { + "name": "Rty - kontura", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "rty", + "price": 2500, + "duration": 2 + }, + { + "name": "3D Rty (kontura a stínování), Full Lips (plné rty)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "3d-rty", + "price": 3500, + "duration": 2.5 + }, + { + "name": "Aquarelle Lips (přirodní stínování, bez kontury)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "aquarelle", + "price": 3000, + "duration": 2 + }, + { + "name": "První korekce po aplikaci pmu max. do 3 měsíců", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "korekce", + "price": 1000, + "duration": 1.5 + }, + { + "name": "Oprava práce obočí jiného salonu", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "oprava-oboci", + "price": "na domluvě", + "duration": "na domluvě" + } + ] +} diff --git a/src/content/services/vakuslim.json b/src/content/services/vakuslim.json new file mode 100644 index 0000000..c88dc36 --- /dev/null +++ b/src/content/services/vakuslim.json @@ -0,0 +1,41 @@ +{ + "$schema": "/src/routes/sluzby/schema.json", + "category": "Vakuslim 48 - zeštíhlující procedura", + "items": [ + { + "name": "Ošetření horních končetin", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "vakuslim-48-zestihlujici-procedura-horni-koncetiny", + "price": 600, + "duration": 2 + }, + { + "name": "1 ošetření spodní části těla (břicho, boky, dolní končetiny)", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "vakuslim-48-zestihlujici-procedura-spodni-cast-tela", + "price": 800, + "duration": 2 + }, + { + "name": "1 ošetření komplet horní-dolní části", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "vakuslim-48-zestihlujici-procedura-komplet-horni-dolni-cast", + "price": 1200, + "duration": 2 + }, + { + "name": "6 ošetření předplatné kompet", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "vakuslim-48-zestihlujici-procedura-6-o-setreni-predplatne-kompet", + "price": 6600, + "duration": 2 + }, + { + "name": "12 ošetření předplatné komplet", + "description": "Diagnostika pleti, odlíčení tonizace", + "id": "vakuslim-48-zestihlujici-procedura-12-o-setreni-predplatne-komplet", + "price": 11000, + "duration": 2 + } + ] +} diff --git a/src/cspDirectives.js b/src/cspDirectives.ts similarity index 60% rename from src/cspDirectives.js rename to src/cspDirectives.ts index 696f8d2..3c8b544 100644 --- a/src/cspDirectives.js +++ b/src/cspDirectives.ts @@ -1,25 +1,30 @@ +// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab + import { PUBLIC_DOMAIN, PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, - PUBLIC_WORKER_URL, -} from '$env/static/public'; + PUBLIC_SENTRY_ORG_ID, + PUBLIC_WORKER_URL, + } from '$env/static/public'; - -const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev +export const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev const directives = { 'base-uri': ["'self'"], - 'child-src': ["'self'"], + 'child-src': ["'self'", 'blob:'], // 'connect-src': ["'self'", 'ws://localhost:*'], 'connect-src': [ "'self'", 'ws://localhost:*', + 'https://*.sentry.io', 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://*.cartocdn.com', + PUBLIC_DOMAIN, PUBLIC_WORKER_URL, ], - 'img-src': ["'self'", 'data:'], + 'img-src': ["'self'", 'data:', 'https://images.unsplash.com'], 'font-src': ["'self'", 'data:'], 'form-action': ["'self'"], 'frame-ancestors': ["'self'"], @@ -30,6 +35,8 @@ const directives = { // "https://*.facebook.net", 'https://hcaptcha.com', 'https://*.hcaptcha.com', + 'https://www.openstreetmap.org', + 'https://*.cartocdn.com' ], 'manifest-src': ["'self'"], 'media-src': ["'self'", 'data:'], @@ -61,13 +68,20 @@ const directives = { 'https://*.hcaptcha.com', 'https://*.sentry.io', // 'https://polyfill.io', + 'https://*.cartocdn.com' ], - 'worker-src': ["'self'"], - // remove report-to & report-uri if you do not want to use Sentry reporting + 'worker-src': [ + "'self'", + 'blob:' + ], + //report-to can throw "Content-Security-Policy: Couldn’t process unknown directive ‘report-to’", leave it for older browsers. 'report-to': ["'csp-endpoint'"], 'report-uri': [ - `https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`, + + `https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`, ], }; -export default directives; +export const csp = Object.entries(directives) + .map(([key, arr]) => key + ' ' + arr.join(' ')) + .join('; '); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index f46fef6..edb6b0b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,50 +1,47 @@ +import type { Handle } from '@sveltejs/kit'; import { sequence } from "@sveltejs/kit/hooks"; import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit"; -import { cspDirectives } from './cspDirectives.js' import * as Sentry from '@sentry/sveltekit'; import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_SENTRY_ORG_ID } from '$env/static/public'; +import { csp, rootDomain } from './cspDirectives'; + Sentry.init({ dsn: 'https://962a7ed3891a335e112746e5c6c6bf42@o4505828687478784.ingest.us.sentry.io/4506871754326016', tracesSampleRate: 1.0, }); -const csp = Object.entries(cspDirectives) - .map(([key, arr]) => key + ' ' + arr.join(' ')) - .join('; '); - -export const cspHandle = async ({ event, resolve }) => { +export const cspHandle: Handle = async ({ event, resolve }) => { + if (!csp) { + throw new Error('csp is undefined'); + } const response = await resolve(event); - response.headers.set('X-Frame-Options', 'SAMEORIGIN'); - response.headers.set('Referrer-Policy', 'no-referrer'); - response.headers.set( - 'Permissions-Policy', - 'accelerometer=(), autoplay=(), camera=(), document-domain=(), encrypted-media=(), fullscreen=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()', - ); - response.headers.set('X-Content-Type-Options', 'nosniff'); - /* Switch from Content-Security-Policy-Report-Only to Content-Security-Policy once you are satisifed policy is what you want - * on switch comment out the Report-Only line - */ - response.headers.set('Content-Security-Policy-Report-Only', csp); - // response.headers.set('Content-Security-Policy', csp); - response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); - response.headers.set( - 'Expect-CT', - `max-age=86400, report-uri="https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`, - ); - response.headers.set( - 'Report-To', - `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`, - ); + + // Permission fullscreen necessary for maps fullscreen + const headers = { + 'X-Frame-Options': 'SAMEORIGIN', + 'Referrer-Policy': 'no-referrer', + 'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`, + 'X-Content-Type-Options': 'nosniff', + // 'Content-Security-Policy-Report-Only': csp, + 'Content-Security-Policy': csp, + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload', + 'Expect-CT': `max-age=86400, report-uri="https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`, + 'Report-To': `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`, + }; + + Object.entries(headers).forEach(([key, value]) => { + response.headers.set(key, value); + }); return response; } - // If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function. -export const handle = sequence(sentryHandle(), cspHandle()); +export const handle: Handle = sequence(sentryHandle(), cspHandle); // If you have a custom error handler, pass it to `handleErrorWithSentry` export const handleError = handleErrorWithSentry(); diff --git a/src/lib/components/Logo.svelte b/src/lib/components/Logo.svelte index e9d9778..7b61a61 100644 --- a/src/lib/components/Logo.svelte +++ b/src/lib/components/Logo.svelte @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/lib/components/Spinner.svelte b/src/lib/components/Spinner.svelte new file mode 100644 index 0000000..b161dde --- /dev/null +++ b/src/lib/components/Spinner.svelte @@ -0,0 +1,57 @@ + + diff --git a/src/lib/components/services/ServiceCard.svelte b/src/lib/components/services/ServiceCard.svelte index 7c17d54..a25bde3 100644 --- a/src/lib/components/services/ServiceCard.svelte +++ b/src/lib/components/services/ServiceCard.svelte @@ -1,12 +1,15 @@ - +
-
Post + Post
@@ -29,7 +32,8 @@