Compare commits

..

No commits in common. "5bf94b1db4d0f075a50e942952571df3131d0a90" and "3919162c6b9916a1a6df96d21a2d00803841f12c" have entirely different histories.

39 changed files with 185 additions and 1016 deletions

View File

@ -1,78 +1,38 @@
# 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`
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Webapp stack
## Creating a project
### [Sveltekit with advanced config](https://kit.svelte.dev/docs/introduction)
If you're seeing this, you've probably already done this step. Congrats!
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.
```bash
# create a new project in the current directory
npm create svelte@latest
### [Skeleton UI library](https://skeleton.dev)
# create a new project in my-app
npm create svelte@latest my-app
```
This is perhaps the best UI library for sveltekit, their docs and their code is great
## Developing
### Tailwind + PostCSS + Fontawesome local
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
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.
```bash
npm run dev
### Most importantly, SCREW CMS, use .md or json or yaml and parse them
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
#### Motivation
## Building
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.
To create a production version of your app:
- 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
```bash
npm run build
```
#### Use case
You can preview the production build with `npm run preview`.
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,
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@ -1,15 +1,12 @@
{
"name": "kkosmetickysalon",
"name": "cosmeticstudio",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"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",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "pnpm run test:integration && npm run test:unit",
"test": "npm 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 .",
@ -25,8 +22,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@sentry/sveltekit": "^7.107.0",
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"ajv": "^8.12.0"
"@sveltejs/vite-plugin-svelte": "^3.0.2"
},
"devDependencies": {
"@floating-ui/dom": "1.5.3",
@ -48,7 +44,6 @@
"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",
@ -56,5 +51,6 @@
"vite": "^5.1.6",
"vite-plugin-tailwind-purgecss": "^0.2.0",
"vitest": "^0.32.4"
}
},
"type": "module"
}

View File

@ -1,34 +0,0 @@
{
"$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
}
]
}

View File

@ -1,69 +0,0 @@
{
"$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
}
]
}

View File

@ -1,48 +0,0 @@
{
"$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
}
]
}

View File

@ -1,69 +0,0 @@
{
"$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ě"
}
]
}

View File

@ -1,41 +0,0 @@
{
"$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
}
]
}

View File

@ -1,30 +1,25 @@
// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab
import {
PUBLIC_DOMAIN,
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID,
PUBLIC_WORKER_URL,
} from '$env/static/public';
} from '$env/static/public';
export const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev
const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev
const directives = {
'base-uri': ["'self'"],
'child-src': ["'self'", 'blob:'],
'child-src': ["'self'"],
// '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:', 'https://images.unsplash.com'],
'img-src': ["'self'", 'data:'],
'font-src': ["'self'", 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
@ -35,8 +30,6 @@ 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:'],
@ -68,20 +61,13 @@ const directives = {
'https://*.hcaptcha.com',
'https://*.sentry.io',
// 'https://polyfill.io',
'https://*.cartocdn.com'
],
'worker-src': [
"'self'",
'blob:'
],
//report-to can throw "Content-Security-Policy: Couldnt process unknown directive report-to", leave it for older browsers.
'worker-src': ["'self'"],
// remove report-to & report-uri if you do not want to use Sentry reporting
'report-to': ["'csp-endpoint'"],
'report-uri': [
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`,
`https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`,
],
};
export const csp = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');
export default directives;

View File

@ -1,47 +1,50 @@
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,
});
export const cspHandle: Handle = async ({ event, resolve }) => {
if (!csp) {
throw new Error('csp is undefined');
}
const csp = Object.entries(cspDirectives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');
export const cspHandle = async ({ event, resolve }) => {
const response = await resolve(event);
// 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);
});
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}"}]}`,
);
return response;
}
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
export const handle: Handle = sequence(sentryHandle(), cspHandle);
export const handle = sequence(sentryHandle(), cspHandle());
// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();

View File

@ -1,5 +1,5 @@
<script lang="ts">
export let clazz = "w-8";
export let clazz: string = "";
</script>
<svg

View File

@ -24,7 +24,7 @@
<section class=" sm:inline-flex space-x-1">
<a
class="btn-icon hover:variant-soft-primary"
class="brand-icon"
href="{config.instagram}"
target="_blank"
rel="noreferrer"
@ -32,7 +32,7 @@
<i class="fa-brands fa-instagram text-lg" />
</a>
<a
class="btn-icon hover:variant-soft-primary"
class="brand-icon"
href="{config.facebook}"
target="_blank"
rel="noreferrer"
@ -53,4 +53,8 @@
</AppBar>
<style lang="postcss">
.brand-icon {
@apply btn-icon hover:variant-soft-primary;
}
</style>

View File

@ -1,57 +0,0 @@
<script lang="ts">
//class="fill-primary-500 stroke-primary-500"
//fill="#FF156D" stroke="#FF156D"
</script>
<svg class="h-6 w-6 md:h-10 md:w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"
><circle class="fill-primary-500 stroke-primary-500" stroke-width="15" r="15" cx="35" cy="100"
><animate
attributeName="cx"
calcMode="spline"
dur="2"
values="35;165;165;35;35"
keySplines="0 .1 .5 1;0 .1 .5 1;0 .1 .5 1;0 .1 .5 1"
repeatCount="indefinite"
begin="0"
/></circle
><circle class="fill-primary-500 stroke-primary-500" stroke-width="15" opacity=".8" r="15" cx="35" cy="100"
><animate
attributeName="cx"
calcMode="spline"
dur="2"
values="35;165;165;35;35"
keySplines="0 .1 .5 1;0 .1 .5 1;0 .1 .5 1;0 .1 .5 1"
repeatCount="indefinite"
begin="0.05"
/></circle
><circle class="fill-primary-500 stroke-primary-500" stroke-width="15" opacity=".6" r="15" cx="35" cy="100"
><animate
attributeName="cx"
calcMode="spline"
dur="2"
values="35;165;165;35;35"
keySplines="0 .1 .5 1;0 .1 .5 1;0 .1 .5 1;0 .1 .5 1"
repeatCount="indefinite"
begin=".1"
/></circle
><circle class="fill-primary-500 stroke-primary-500" stroke-width="15" opacity=".4" r="15" cx="35" cy="100"
><animate
attributeName="cx"
calcMode="spline"
dur="2"
values="35;165;165;35;35"
keySplines="0 .1 .5 1;0 .1 .5 1;0 .1 .5 1;0 .1 .5 1"
repeatCount="indefinite"
begin=".15"
/></circle
><circle class="fill-primary-500 stroke-primary-500" stroke-width="15" opacity=".2" r="15" cx="35" cy="100"
><animate
attributeName="cx"
calcMode="spline"
dur="2"
values="35;165;165;35;35"
keySplines="0 .1 .5 1;0 .1 .5 1;0 .1 .5 1;0 .1 .5 1"
repeatCount="indefinite"
begin=".2"
/></circle
></svg
>

View File

@ -1,15 +1,12 @@
<script lang="ts">
import { getImageLink } from '$lib/images';
import type { Service } from '$lib/types/service';
import convertMinutesToHours from '$lib/utils/minToH';
export let item: Service['items'][number];
import { getImageLink } from "$lib/images";
export let item: any = {};
</script>
<a href="/sluzby/{item.id}" id="{item.id}"class="w-72 card variant-glass-secondary mx-2 my-4 duration-500 hover:scale-105 hover:shadow-xl shadow-md">
<a href="/sluzby/{item.id}" class="w-72 card variant-glass-secondary mx-2 my-4 duration-500 hover:scale-105 hover:shadow-xl shadow-md">
<header>
<img src={getImageLink({id: item.id, w: 288, h: 320 })} class="bg-black/50 object-cover aspect-[9/10] rounded-t-xl" alt="Post" loading="lazy" />
<img src={getImageLink({id: item.id, w: 288, h: 320 })} class="bg-black/50 object-cover aspect-[9/10] rounded-t-xl" alt="Post" />
</header>
<!-- <div class="img" style="background-image: url('/images/services/{item.id}.jpg');"/> -->
<div class="flex flex-col px-4 py-3 w-72 h-full">
@ -32,8 +29,7 @@
<footer class="justify-self-end align-bottom">
<div class="flex flex-col md:flex-row md:justify-between items-center">
<p class=" text-lg text-surface-900 font-semibold text-right">
{typeof item.price === 'number' ? `${item.price},-` : item.price}
{typeof item.duration === 'number' ? `/${convertMinutesToHours(item.duration)}` : ''}
{typeof item.price === 'number' ? `${item.price},-` : item.price}{typeof item.duration === 'number' ? `/${item.duration}h` : ''}
</p>
<a
href="https://app.cal.com/kkosmetickysalon/{item.id}"

View File

@ -1,4 +1,3 @@
<script lang="ts">
import type { Service } from '$lib/types/service';
import ServiceCard from '$lib/components/services/ServiceCard.svelte';
@ -31,13 +30,13 @@
<style lang="postcss">
/* .services-container {
.services-container {
@apply grid md:grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 p-4;
}
@media (max-width: 768px) {
.services-container {
@apply grid-cols-1;
}
}*/
}
</style>

View File

@ -8,18 +8,3 @@ export const author = 'Klára Morinová';
export const email = 'klara@kkosmetickysalon.cz';
export const facebook = 'https://www.facebook.com/jack.morin.712';
export const instagram = 'https://www.instagram.com/kkosmetickysalon/';
// prettier-ignore
export const socialLinks = [
{ title: 'Instagram', href: 'https://www.instagram.com/kkosmetickysalon/', icon: 'fa-brands fa-linkedin'},
{ title: 'Phone', href: 'tel:+420792304497', icon: './MatrixLogo' },
{ title: 'Facebook', href: 'hhttps://www.facebook.com/jack.morin.712', icon: 'fa-brands fa-facebook'},
{ title: 'Email', href: 'klara@kkosmetickysalon.cz', icon: 'fa-regular fa-envelope'},
];
// Routes
export const NavRoutes = [
{ title: 'Home', href: '/' },
{ title: 'Sluzby', href: '/Sluzby' },
{ title: 'O-mne', href: '/o-mne' }
];

View File

@ -1,21 +0,0 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import type { InstagramPost } from '../types/instagram';
import { derived } from "svelte/store";
import { navigating } from "$app/stores";
export const instagramFeed: Writable<InstagramPost[]> = writable([]);
let timer = null;
/** When navigating from to, time 500ms to be true, else false */
export const navigationIsDelayed = derived(navigating, (newValue, set) => {
if (timer) { clearTimeout(timer); }
if (newValue) {
timer = setTimeout(() => set(true), 500);
}
set(false);
});

View File

@ -1,20 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function convertMinutesToHours(minutes) {
try {
var hours = Math.floor(minutes / 60);
var mins = minutes % 60;
if (hours) {
console.log("".concat(hours, "h ").concat(mins, "m"));
return "".concat(hours, "h ").concat(mins, "m");
}
else
console.log("".concat(mins, "m"));
return "".concat(mins, "m");
}
catch (error) {
console.error("I don't think that's time: ".concat(error));
process.exit(1); // Exit with non-zero code to signal failure
}
}
exports.default = convertMinutesToHours;

View File

@ -1,16 +0,0 @@
export default function convertMinutesToHours(minutes:number) {
try {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours) {
console.log(`${hours}h ${mins}m`)
return `${hours}h ${mins}m`
}
else console.log(`${mins}m`); return `${mins}m`
} catch (error) {
console.error(`I don't think that's time: ${error}`);
process.exit(1); // Exit with non-zero code to signal failure
}
}

View File

@ -1,36 +0,0 @@
<script>
import { page } from '$app/stores'
</script>
{#if $page.status === 404}
<div class="flex flex-col items-center m-4 py-8">
<div
class="relative mb-[3.33vh] flex h-24 w-24 md:h-64 md:w-64 items-center justify-center bg-surface-800"
>
<img
class="object-fill"
src="/animations/travolta_confused.webp"
alt="Travolta Confused gif"
/>
<h1 class="h1 absolute leading-[5rem] text-white">404</h1>
</div>
<h2 class="h4 mb-2">
Bože můj, co tu děláte?
</h2>
<p>Tato stránka neexistuje, buď byla odstraněna nebo se jednoduše vytratila z vesmíru.</p>
</div>
{:else}
<h2>{$page.status}</h2>
{#if $page.error}
<p class="subhead">{$page.error.message}</p>
{/if}
<p><strong>Promiňte!</strong> Stal se strašlivý omyl. Možná byste mohli zkusit nějaké z následujících stránek?</p>
<ul>
<li><a href="/">Domů</a></li>
<li><a href="/sluzby">Služby</a></li>
<li><a href="/kontakt">Projects</a></li>
</ul>
{/if}

View File

@ -1,7 +1,7 @@
<script lang="ts">
// Styleshit
import '$src/app.postcss';
import '../app.postcss';
// Font Gruesome
import '@fortawesome/fontawesome-free/css/fontawesome.css';
@ -9,12 +9,7 @@
import '@fortawesome/fontawesome-free/css/solid.css';
// Components & Utilities
import { AppShell, Drawer, initializeStores, getDrawerStore } from '@skeletonlabs/skeleton';
import { } from '@skeletonlabs/skeleton';
initializeStores();
const drawerStore = getDrawerStore();
$: positionClasses = $drawerStore.open ? 'translate-x-[50%]' : '';
import { AppShell } from '@skeletonlabs/skeleton';
import { page } from '$app/stores';
// Floating UI for Popups
@ -47,21 +42,13 @@
}
};
// Scroll to anchor
$: if ($page.url.pathname) {
// Workaround until https://github.com/sveltejs/kit/issues/2664 is fixed
if (typeof window !== 'undefined' && window.location.hash) {
const deepLinkedElement = document.getElementById(window.location.hash.substring(1));
if (deepLinkedElement) {
deepLinkedElement.scrollIntoView();
}
}
// Scroll heading into view
function scrollHeadingIntoView(): void {
if (!window.location.hash) return;
const elemTarget: HTMLElement | null = document.querySelector(window.location.hash);
if (elemTarget) elemTarget.scrollIntoView({ behavior: 'smooth' });
};
import { fade } from 'svelte/transition';
import Spinner from '$lib/components/Spinner.svelte';
import { navigationIsDelayed } from '$lib/stores';
</script>
</script>
<!-- SEO -->
<svelte:head>
@ -93,15 +80,7 @@
<!-- <Analytics /> -->
<!-- App Shell -->
<Drawer />
{#if $navigationIsDelayed}
<div class="fixed w-full h-full z-10" in:fade={{ duration: 150 }}>
<div class="absolute w-full h-full flex justify-center items-center z-20">
<Spinner />
</div>
</div>
{:else}
<AppShell class="transition-transform {positionClasses}">
<AppShell>
<svelte:fragment slot="header">
<MainHeader />
</svelte:fragment>
@ -112,8 +91,7 @@
<svelte:fragment slot="footer">
<MainFooter />
</svelte:fragment>
</AppShell>
{/if}
</AppShell>
<!-- <CookieConsent />
<Segment /> -->

View File

@ -1,20 +1,13 @@
import { error } from '@sveltejs/kit';
import { RequestHandler} from './$types';
import { IG_API_KEY } from '$env/static/private';
export const POST: RequestHandler = async ({ request }) => {
export async function GET() {
try {
const { next } = await request.json();
const response = await fetch(next, {
method: 'GET',
});
const response = await fetch(`https://graph.instagram.com/me/media?fields=id,media_type,media_url,caption,timestamp&access_token=${IG_API_KEY}`);
const data = await response.json();
return new Response(data, {
headers: {
'Content-Type': 'application/json'
}
});
return new Response(JSON.stringify(data));
} catch (err) {
console.log('Error: ', err);
error(500, 'Error retrieving data in /api/instagram.json');
throw error(500, 'Error retrieving Instagram data');
}
}

View File

@ -1,45 +0,0 @@
// === THANKS TO https://github.com/rodneylab/sveltekit-hcaptcha-form/blob/main/src/routes/api/verify/%2Bserver.js
// I cleaned it and converted to TS
import { HCAPTCHA_SECRETKEY } from '$env/static/private';
import { PUBLIC_HCAPTCHA_SITEKEY } from '$env/static/public';
import { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => {
try {
const { name, email, message, response: hCaptchaClientResponse } = await request.json();
// const secret = HCAPTCHA_SECRETKEY;
// const sitekey = PUBLIC_HCAPTCHA_SITEKEY;
const body = new URLSearchParams({ response: hCaptchaClientResponse, HCAPTCHA_SECRETKEY, PUBLIC_HCAPTCHA_SITEKEY });
const response = await fetch('https://hcaptcha.com/siteverify', {
method: 'POST',
credentials: 'omit',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body.toString(),
});
const data = await response.json();
const { success } = data;
console.log('data: ', data);
if (success) {
console.log('hCaptcha says yes!');
} else {
console.log('hCaptcha says no!');
}
// process name, email and message here e.g. email site admin with message details
console.log({ name, email, message });
return new Response('OK');
} catch (err) {
const error = `HCaptcha Error in /verify.json.js: ${err}`;
console.error(error);
return {
status: 500,
error,
};
}
}

View File

@ -1,22 +0,0 @@
import { error } from '@sveltejs/kit';
import { IG_ACCESS_TOKEN } from '$env/static/private';
import { PageServerLoad} from './$types';
export const load: PageServerLoad = async () => {
try {
const url = `https://graph.instagram.com/me/media?fields=caption,id,media_type,media_url,timestamp&access_token=${IG_ACCESS_TOKEN}`;
const response = await fetch(url, {
method: 'GET',
});
const data = await response.json();
return new Response(data, {
headers: {
'Content-Type': 'application/json'
}
});
} catch (err) {
console.log('Error: ', err);
throw error(500, 'Error retrieving Instagram data');
}
}

View File

@ -1,5 +0,0 @@
<script lang="ts">
import { PUBLIC_MAPBOX_ACCESS_TOKEN } from '$env/static/public';
</script>

View File

@ -1,97 +0,0 @@
<!--
This is just a very simple page with a button to throw an example error.
Feel free to delete this file and the entire sentry route.
-->
<script>
import * as Sentry from '@sentry/sveltekit';
function getSentryData() {
Sentry.startSpan({
name: 'Example Frontend Span',
op: 'test',
}, async () => {
const res = await fetch('/sentry-example');
if (!res.ok) {
throw new Error('Sentry Example Frontend Error');
}
});
}
</script>
<div>
<head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your SvelteKit app!" />
</head>
<main>
<h1>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 44">
<path
fill="currentColor"
d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
/>
</svg>
</h1>
<p>
Get Started with this <strong>simple Example:</strong>
</p>
<p>1. Send us a sample error:</p>
<button
type="button"
on:click={getSentryData}>
Throw error!
</button>
<p>
2. Look for the error on the
<a href="https://none-b0c3fadae.sentry.io/issues/?project=4506871754326016">Issues Page</a>.
</p>
<p style="margin-top: 24px;">
For more information, take a look at the
<a href="https://docs.sentry.io/platforms/javascript/guides/sveltekit/">
Sentry SvelteKit Documentation
</a>
</p>
</main>
</div>
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
font-size: 4rem;
margin: 14px 0;
}
svg {
height: 1em;
}
button {
padding: 12px;
cursor: pointer;
background-color: rgb(54, 45, 89);
border-radius: 4px;
border: none;
color: white;
font-size: 1em;
margin: 1em;
transition: all 0.25s ease-in-out;
}
button:hover {
background-color: #8c5393;
box-shadow: 4px;
box-shadow: 0px 0px 15px 2px rgba(140, 83, 147, 0.5);
}
button:active {
background-color: #c73852;
}
</style>

View File

@ -1,6 +0,0 @@
// This is just a very simple API route that throws an example error.
// Feel free to delete this file and the entire sentry route.
export const GET = async () => {
throw new Error("Sentry Example API Route Error");
};

View File

@ -1,22 +1,22 @@
<script lang="ts">
import ServicesLayout from '$lib/components/services/ServicesLayout.svelte';
import type { Service } from '$lib/types/service';
export const id = '';
let id: string;
// Locally populated services
let services: Service[] = [
{
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ě' }
{ name: 'Obočí Pudrové, Ombré', id: 'oboci',price: 3000, duration: 2.5 },
{ name: 'Horní linky - meziřasové přirozené', id: 'linky', price: 2000, duration: 2 },
{ name: 'Klasické linky - s ocáskem', id: 'classic-linky', price: 3000, duration: 2.5 },
{ name: 'Klasické linky - s ocáskem + spodní linky', id: 'classic-linky+spodni', price: 3500, duration: 2.5 },
{ name: 'Rty - kontura', id: 'rty', price: 2500, duration: 2 },
{ name: '3D Rty (kontura a stínování), Full Lips (plné rty)', id: '3d-rty', price: 3500, duration: 2.5 },
{ name: 'Aquarelle Lips (přirodní stínování, bez kontury)', id: 'aquarelle', price: 3000, duration: 2 },
{ name: 'První korekce po aplikaci pmu max. do 3 měsíců', id: 'korekce', price: 1000, duration: 1.5 },
{ name: 'Oprava práce obočí jiného salonu', id: 'oprava-oboci', price: 'na domluvě', duration: 'na domluvě' }
],
},
{
@ -26,41 +26,41 @@
{ 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Í', id: 'relaxacni', 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)', 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 },
{ name: 'CLEAR + ANTI AKNÉ', id: 'clear-anti-akne', price: 690, duration: 1.5 },
{ name: 'Odlíčení + sérum + alginátová maska (PROJASNĚNÍ)', id: 'odliceni-serum-alginatova-maska', price: 300, duration: 1 },
]
},
{
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 },
{ name: 'Lifting řas booster (botox)', id: 'lifting-ras-booster', price: 500, duration: 1 },
{ name: 'Laminace obočí + výživa', id: 'laminace-oboci-vyziva', price: 500, duration: 1 },
{ name: 'Úprava obočí (tvar + barva)', id: 'uprava-oboci-tvar-barva', price: 250, duration: 1 },
{ name: 'Úprava obočí + řasy (tvar + barvení)', id: 'uprava-oboci-rasy-tvar-barveni', price: 300, duration: 1 },
]
},
{
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 }
{ name: 'Depilace Horní ret', id: 'depilace-horni-ret', price: 80, duration: 0.5 },
{ name: 'Depilace Brada', id: 'depilace-brada', price: 80, duration: 0.5 },
{ name: 'Depilace Obočí', id: 'depilace-oboci', price: 150, duration: 0.5 },
{ name: 'Depilace Tváře', id: 'depilace-tvare', price: 150, duration: 0.5 },
{ name: 'Depilace Podpaží', id: 'depilace-podpazi', price: 150, duration: 0.5 },
{ name: 'Depilace Předloktí', id: 'depilace-predlokti', price: 200, duration: 0.5 },
{ name: 'Depilace Celé ruce', id: 'depilace-cele-ruce', price: 350, duration: 1 },
{ name: 'Depilace Lýtka', id: 'depilace-lytka', price: 350, duration: 1 },
{ name: 'Depilace Celé nohy', id: 'depilace-cele-nohy', price: 500, duration: 1 }
]
},
{
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 }
{ name: 'Ošetření horních končetin', 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)', id: 'vakuslim-48-zestihlujici-procedura-spodni-cast-tela', price: 800, duration: 2 },
{ name: '1 ošetření komplet horní-dolní části', id: 'vakuslim-48-zestihlujici-procedura-komplet-horni-dolni-cast', price: 1200, duration: 2 },
{ name: '6 ošetření předplatné kompet', id: 'vakuslim-48-zestihlujici-procedura-6-o-setreni-predplatne-kompet', price: 6600, duration: 2 },
{ name: '12 ošetření předplatné komplet', id: 'vakuslim-48-zestihlujici-procedura-12-o-setreni-predplatne-komplet', price: 11000, duration: 2 }
]
}
];

View File

@ -1,74 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Service",
"type": "object",
"required": [
"category",
"items"
],
"properties": {
"category": {
"type": "string",
"description": "Name of the category of services",
"minLength": 4,
"default": "Permanentní makeup"
},
"items": {
"title": "Items under the category",
"type": "array",
"items": {
"type": "object",
"title": "Items",
"properties": {
"name": {
"type": "string",
"description": "Name of the service"
},
"description": {
"title": "Description",
"type": "string",
"description": "description :D",
"default": "A description of a description"
},
"id": {
"type": "string",
"description": "a short handle used in urls and on cal.com",
"default": "pmu",
"minLength": 3
},
"price": {
"title": "Price",
"description": "Price of the product",
"oneOf": [
{
"type": "number",
"description": "Price of the service as a number",
"default": 300
},
{
"type": "string",
"description": "Price of the service as a text description (e.g., 'Free')",
"default": "na dohode"
}
]
},
"duration": {
"title": "Duration",
"description": "How long will the procedure take? (can be on demand or specific amount)",
"oneOf": [
{
"type": "number",
"description": "Duration of the service in minutes (e.g., 60)",
"minimum": 15
},
{
"type": "string",
"description": "Duration of the service as a text description (e.g., 'on demand')"
}
]
}
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

View File

@ -1,35 +0,0 @@
import adapter from '@sveltejs/adapter-node';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
}),
],
kit: {
adapter: adapter({
out: 'build',
precompress: false
}),
// Aliases need tsconfig explicit inclusion
alias: {
$lib: './src/lib',
$root: './',
$src: './src',
$routes: './src/routes',
},
env: {
publicPrefix: "PUBLIC_",
},
// https://kit.svelte.dev/docs/configuration#alias
},
};
export default config;

32
svelte.config.ts Normal file
View File

@ -0,0 +1,32 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';
import preprocess from 'svelte-preprocess';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { Config } from '@sveltejs/kit';
const __dirname = dirname(fileURLToPath(import.meta.url));
const config: Config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({
postcss: true
}),
vitePreprocess({
style: {
css: {
postcss: join(__dirname, 'postcss.config.cjs')
}
}
})
],
kit: {
adapter: adapter()
// https://kit.svelte.dev/docs/configuration#alias
}
};
export default config;

View File

@ -1,41 +0,0 @@
import Ajv from 'ajv';
import fs from 'fs';
import path from 'path';
const serviceSchema = JSON.parse(fs.readFileSync('./src/routes/sluzby/schema.json', 'utf-8'));
function validateFile(filePath) {
const ajv = new Ajv();
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const validate = ajv.compile(serviceSchema);
const valid = validate(data);
if (!valid) {
throw new Error(`Invalid service data in ${filePath}: ${JSON.stringify(validate.errors)}`);
}
}
function scanDirectory(directory) {
const files = fs.readdirSync(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
scanDirectory(filePath);
} else if (path.extname(filePath) === '.json') {
validateFile(filePath);
}
}
}
export function validateServices() {
try {
scanDirectory('./src/content/services');
console.log('All services validated successfully!');
} catch (error) {
console.error(`Error validating services: ${error}`);
process.exit(1); // Exit with non-zero code to signal failure
}
}
validateServices();

View File

@ -8,27 +8,8 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
// custom compiler options
"noEmit": true,
"target": "ES2018",
"module": "ES2022",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
},
"include": [
"./scripts/**/*",
"./test/*.js",
"./*.js",
"./src/**/*.d.ts",
"./src/**/*.js",
"./src/**/*.svelte",
"./src/**/*.ts",
".svelte-kit/ambient.d.ts",
".svelte-kit/types/**/$types.d.ts",
"./csp-directives.ts"
],
"exclude": ["node_modules/*"]
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes

View File

@ -12,23 +12,15 @@ export default defineConfig({
envPrefix: "PUBLIC_",
plugins: [sentrySvelteKit({
sourceMapsUploadOptions: {
org: "mattmor",
project: "kkosmetickysalon",
//telemetry off
telemetry: false,
org: "none-b0c3fadae",
project: "javascript-sveltekit"
}
}),
sveltekit(),
purgeCss({
}), sveltekit(), purgeCss({
safelist: {
// any selectors that begin with "hljs-" will not be purged
greedy: [/^hljs-/],
},
})],
define: {
'process.env.VITE_BUILD_TIME': JSON.stringify(new Date().toISOString()),
},
})],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},