Compare commits
45 Commits
Author | SHA1 | Date |
---|---|---|
matthieu42morin | f833c9c206 | |
matthieu42morin | 870a6f2016 | |
matthieu42morin | 347bc65360 | |
matthieu42morin | c0ffda8394 | |
Matthieu Morin | aca901c2a7 | |
Matthieu Morin | 0115cb501c | |
Matthieu Morin | 1a75df9851 | |
matthieu42morin | e6203baf9e | |
matthieu42morin | 4465d1d567 | |
Matthieu Morin | 67a2d0be1e | |
Matthieu Morin | c04d27265b | |
Matthieu Morin | 22e59b089f | |
Matthieu Morin | 6ac2261cf1 | |
Matthieu Morin | c78de3ec28 | |
Matthieu Morin | 0e30d0a539 | |
matthieu42morin | 8291443c80 | |
matthieu42morin | d73bac59a8 | |
matthieu42morin | 1998709a8a | |
matthieu42morin | 5107f756e1 | |
matthieu42morin | ff3f943a0d | |
matthieu42morin | 6c3c7ba724 | |
matthieu42morin | ee9b7501e2 | |
matthieu42morin | e9ab134ebd | |
matthieu42morin | efaae142f1 | |
matthieu42morin | bd515e7339 | |
matthieu42morin | 4811bcdd23 | |
matthieu42morin | fdb2b9b4ed | |
matthieu42morin | c08635b4c6 | |
matthieu42morin | d06ed63a75 | |
matthieu42morin | 948ef1ca47 | |
matthieu42morin | 1816858bc6 | |
matthieu42morin | 3db7f22a17 | |
matthieu42morin | 2c939ff741 | |
matthieu42morin | 8187fb58bc | |
matthieu42morin | c584fd1f29 | |
matthieu42morin | 7121aacc7e | |
matthieu42morin | 41e5260767 | |
matthieu42morin | 753fbc76b7 | |
matthieu42morin | 06d84f6d6e | |
matthieu42morin | 75ee13fe94 | |
matthieu42morin | fafb0b1c1e | |
matthieu42morin | 40ddca238e | |
matthieu42morin | e0f0efb590 | |
Matthieu Morin | ea479a1f4e | |
Matthieu Morin | 5bf94b1db4 |
51
README.md
51
README.md
|
@ -1,6 +1,7 @@
|
||||||
# create-svelte
|
# 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`
|
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 either in `/static` or are on [Cloudinary](#cloudinary-with-some-scripting) and are printed at multiple places in multiple formats.
|
||||||
|
The texts are a work of mom, me and Google's Gemini( it's stupid for most complex tasks, but it's good at rewriting text and adding marketing emotions and so on, especially good at Czech unlike the other models)
|
||||||
|
|
||||||
## Webapp stack
|
## Webapp stack
|
||||||
|
|
||||||
|
@ -16,26 +17,34 @@ This is perhaps the best UI library for sveltekit, their docs and their code is
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Most importantly, SCREW CMS, use .md or json or yaml and parse them
|
### Most importantly, SCREW CMS, use .md and json or yaml and parse them
|
||||||
|
|
||||||
#### Motivation
|
#### Motivation
|
||||||
|
|
||||||
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.
|
I have spent considerable amount of time researching and learning the bloatware, complex CMS systems and SaaS models, that are 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 and understand key/value, then you're set. Introducing `.md` is a no brainer to any content writing.
|
||||||
|
|
||||||
- it's dead simple and you literally almost just import it
|
- 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
|
- You don't have to host strapi or a db on a minimum 4GB RAM server in the cloud for making a post once a month and have to maintain the bloat.
|
||||||
- Static makes better SEO
|
- Static makes better SEO
|
||||||
|
|
||||||
#### Use case
|
#### Use case
|
||||||
|
|
||||||
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.
|
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.
|
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.
|
This implementation offers an option to create or edit json files a [JSON editor](https://github.com/json-editor/json-editor) according to a schema to help write new services in a GUI.
|
||||||
The json data is then validated by [ajv](https://ajv.js.org) to match the type Service in `$lib/types/service.d.ts`
|
The json data is validated by [ajv](https://ajv.js.org) with my script in `./tests/ValidateServices.js/` to match the type Service in `$lib/types/service.d.ts`
|
||||||
|
|
||||||
|
### Cloudinary with some scripting
|
||||||
|
|
||||||
|
I automated checking for images in the content folder and uploading them to Cloudinary.
|
||||||
|
I also have a script that generates a json file with all the images in the content folder and uploads them to Cloudinary.
|
||||||
|
This json file is then used to get images as simple keys.
|
||||||
|
|
||||||
## Features, Components and parts
|
## Features, Components and parts
|
||||||
|
|
||||||
A list of mostly
|
### SEO + Opengraph + frontmatter
|
||||||
|
|
||||||
|
my own implementation
|
||||||
|
|
||||||
### [LibreMaps](https://svelte-maplibre.vercel.app/)
|
### [LibreMaps](https://svelte-maplibre.vercel.app/)
|
||||||
|
|
||||||
|
@ -45,11 +54,11 @@ Screw Google Maps, I knew I wanted to use OSM, maybe Mapbox, because I had exper
|
||||||
|
|
||||||
### CSP, custom hooks, custom headers
|
### CSP, custom hooks, custom headers
|
||||||
|
|
||||||
Securing this app with the latest security features and web technologies.
|
Securing this app with the latest security features and web technologies. If you want to look at the CSP and other policies, look at the headers set in `hooks.server.ts`. I use [Sentry](#sentry---runtime-prod--dev-analysis) as the endpoint. It took a lot of experimentation to get this right, it is a very strict policy so any new external service will not be allowed and the policies must be changed.
|
||||||
|
|
||||||
### Service Worker
|
### Service Worker, manifest
|
||||||
|
|
||||||
Why not?
|
Why not? It's cool, build PWAs to screw with Apple.
|
||||||
|
|
||||||
## Admin/DevOps Tools
|
## Admin/DevOps Tools
|
||||||
|
|
||||||
|
@ -59,7 +68,7 @@ A list of mostly 3rd party useful tools this project uses.
|
||||||
|
|
||||||
### hCaptcha
|
### 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.
|
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
|
### Plausible self-hosted
|
||||||
|
|
||||||
|
@ -67,7 +76,7 @@ That or coding some metrics and using some opinionated solution myself.
|
||||||
|
|
||||||
### Sentry - runtime prod & dev analysis
|
### 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.
|
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. I want to spend as little time as possible on monitoring and maintenance and still ensure 100% reliability and performance.
|
||||||
|
|
||||||
### Playwright - headless browser target testing
|
### Playwright - headless browser target testing
|
||||||
|
|
||||||
|
@ -75,4 +84,20 @@ TODO, probably sometime, It can be useful with the service posts.
|
||||||
|
|
||||||
### Dockerfile
|
### 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,
|
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.
|
||||||
|
|
||||||
|
## Some notable references and minor features
|
||||||
|
|
||||||
|
### Email obfuscation
|
||||||
|
|
||||||
|
Spammers are hopefully not so smart. I sveltyfied a [SVG email obfuscation method (because JS bad) from https://rouninmedia.github.io/protecting-your-email-address-via-svg-instead-of-js/](https://rouninmedia.github.io/protecting-your-email-address-via-svg-instead-of-js/).
|
||||||
|
|
||||||
|
### OpenGraph
|
||||||
|
|
||||||
|
My own implementation, extending the Service type to still adhere to one central source of info.
|
||||||
|
|
||||||
|
### Fonts (look at `./static`)
|
||||||
|
|
||||||
|
- Playfair Display
|
||||||
|
- Quicksand
|
||||||
|
- Montferrat
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
|
||||||
|
import remarkExternalLinks from 'remark-external-links';
|
||||||
|
import remarkSetImagePath from './src/lib/utils/remark-set-image-path.js';
|
||||||
|
import remarkLinkWithImageAsOnlyChild from './src/lib/utils/remark-link-with-image-as-only-child.js';
|
||||||
|
import rehypeImgSize from 'rehype-img-size';
|
||||||
|
|
||||||
|
import remarkUnwrapImages from 'remark-unwrap-images';
|
||||||
|
import remarkToc from 'remark-toc';
|
||||||
|
import rehypeSlug from 'rehype-slug';
|
||||||
|
|
||||||
|
// import { highlightCode } from './src/lib/utils/highlighter.js';
|
||||||
|
|
||||||
|
/** @type {import('mdsvex').MdsvexOptions} */
|
||||||
|
const config = defineConfig({
|
||||||
|
extensions: ['.svelte.md', '.md', '.svx'],
|
||||||
|
smartypants: {
|
||||||
|
dashes: 'oldschool'
|
||||||
|
},
|
||||||
|
/* Wait for skeleton to implement Prismjs, for now use <CodeBlock /> in .md files */
|
||||||
|
// layout: {
|
||||||
|
// blog: './src/lib/components/blog/_blog-layout.svelte',
|
||||||
|
// project: './src/lib/components/projects/_project-layout.svelte',
|
||||||
|
// _: './src/lib/components/fallback/_layout.svelte'
|
||||||
|
// },
|
||||||
|
/* Plugins */
|
||||||
|
rehypePlugins: [
|
||||||
|
[rehypeSlug],
|
||||||
|
[rehypeImgSize]
|
||||||
|
// [
|
||||||
|
// /** Custom rehype plugin to add loading="lazy" to all images */
|
||||||
|
// () => {
|
||||||
|
// return (tree) => {
|
||||||
|
// visit(tree, 'element', (node) => {
|
||||||
|
// if (node.tagName === 'img') {
|
||||||
|
// node.properties.loading = 'lazy';
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
],
|
||||||
|
remarkPlugins: [
|
||||||
|
[remarkToc, { maxDepth: 3, tight: true }],
|
||||||
|
[
|
||||||
|
(remarkExternalLinks,
|
||||||
|
{
|
||||||
|
target: '_blank'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
[remarkUnwrapImages],
|
||||||
|
remarkSetImagePath,
|
||||||
|
remarkLinkWithImageAsOnlyChild
|
||||||
|
// [
|
||||||
|
// headings,
|
||||||
|
// {
|
||||||
|
// behavior: 'append',
|
||||||
|
// linkProperties: {},
|
||||||
|
// content: function (node) {
|
||||||
|
// return [
|
||||||
|
// h('span.icon.icon-link header-anchor', {
|
||||||
|
// ariaLabel: toString(node) + ' permalink'
|
||||||
|
// })
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// remarkHeadingsPermaLinks,
|
||||||
|
// getHeadings
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default config;
|
22
package.json
22
package.json
|
@ -4,10 +4,12 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"validate": "node ./tests/ValidateServices.js",
|
"validate:images": "node ./tests/validateImages.js",
|
||||||
|
"validate:services": "node ./tests/ValidateServices.js",
|
||||||
|
"validate": "pnpm run validate:services && validate:images",
|
||||||
"dev": "pnpm run validate && vite dev --mode development",
|
"dev": "pnpm run validate && vite dev --mode development",
|
||||||
"build": "pnpm run validate && vite build",
|
"build": "pnpm run validate && vite build",
|
||||||
"build-dev": "pnpm run validate && vite build --mode development",
|
"build-dev": "pnpm run vite build --mode development",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "pnpm 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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
@ -24,7 +26,11 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@sentry/sveltekit": "^7.107.0",
|
"@sentry/sveltekit": "^7.107.0",
|
||||||
|
"@sveltejs/adapter-cloudflare": "^4.4.0",
|
||||||
|
"@sveltejs/adapter-netlify": "^4.2.0",
|
||||||
"@sveltejs/adapter-node": "^5.0.1",
|
"@sveltejs/adapter-node": "^5.0.1",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
|
"@sveltejs/adapter-vercel": "^5.3.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"ajv": "^8.12.0"
|
"ajv": "^8.12.0"
|
||||||
},
|
},
|
||||||
|
@ -33,6 +39,7 @@
|
||||||
"@playwright/test": "^1.42.1",
|
"@playwright/test": "^1.42.1",
|
||||||
"@skeletonlabs/skeleton": "^2.9.0",
|
"@skeletonlabs/skeleton": "^2.9.0",
|
||||||
"@skeletonlabs/tw-plugin": "^0.2.4",
|
"@skeletonlabs/tw-plugin": "^0.2.4",
|
||||||
|
"@sveltejs/amp": "^1.1.0",
|
||||||
"@sveltejs/kit": "^2.5.4",
|
"@sveltejs/kit": "^2.5.4",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
@ -40,12 +47,22 @@
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.18",
|
||||||
|
"cloudinary": "^2.1.0",
|
||||||
|
"dropcss": "^1.0.16",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.35.1",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"mdsvex": "^0.11.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
"rehype-img-size": "^1.0.1",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"remark-external-links": "^9.0.1",
|
||||||
|
"remark-slug": "^7.0.1",
|
||||||
|
"remark-toc": "^9.0.0",
|
||||||
|
"remark-unwrap-images": "^4.0.0",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.7",
|
"svelte-check": "^3.6.7",
|
||||||
"svelte-maplibre": "^0.8.2",
|
"svelte-maplibre": "^0.8.2",
|
||||||
|
@ -53,6 +70,7 @@
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
|
"unist-util-visit": "^5.0.0",
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.1.6",
|
||||||
"vite-plugin-tailwind-purgecss": "^0.2.0",
|
"vite-plugin-tailwind-purgecss": "^0.2.0",
|
||||||
"vitest": "^0.32.4"
|
"vitest": "^0.32.4"
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
// updateMarkdownImages.js
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { getCloudinaryImageUrl } from './imageUtils';
|
||||||
|
|
||||||
|
|
||||||
|
const postsDir = './posts';
|
||||||
|
|
||||||
|
fs.readdirSync(postsDir).forEach((folder) => {
|
||||||
|
const markdownFiles = fs.readdirSync(path.join(postsDir, folder)).filter((file) => file.endsWith('.md'));
|
||||||
|
|
||||||
|
markdownFiles.forEach((file) => {
|
||||||
|
const filePath = path.join(postsDir, folder, file);
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
// Regex to find image references in markdown
|
||||||
|
const imageRegex = /!\[.*?\]\((.*?)\)/g;
|
||||||
|
content = content.replace(imageRegex, (match, p1) => {
|
||||||
|
const imageUrl = getCloudinaryImageUrl(p1, 300, 300); // Example width and height
|
||||||
|
return `![${p1}](${imageUrl})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="light">
|
<html lang="en" class="light" amp>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"example_image": {
|
||||||
|
"publicId": "example_image",
|
||||||
|
"transformations": [
|
||||||
|
{ "width": 1200, "height": 627, "crop": "fill", "quality": "auto", "format": "auto" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"another_image": {
|
||||||
|
"publicId": "another_image",
|
||||||
|
"transformations": [
|
||||||
|
{ "width": 800, "height": 600, "crop": "fill", "quality": "auto", "format": "auto" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Service category schema",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["title", "description", "id", "services"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the category of services",
|
||||||
|
"minLength": 4,
|
||||||
|
"default": "Permanentní makeup"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Description of the category",
|
||||||
|
"type": "string",
|
||||||
|
"description": "description :D",
|
||||||
|
"default": "A description of a description"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "a short handle used in urls",
|
||||||
|
"default": "pmu",
|
||||||
|
"minLength": 3
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"title": "Image",
|
||||||
|
"description": "A featured image in previews and on top of page",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"title": "tags",
|
||||||
|
"description": "Tags of the services, e.g. 'pmu'",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"title": "services under the category",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "schema-services.json#"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "schema-services.json",
|
||||||
|
"title": "Service schema",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["title", "id", "duration", "price"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"title": "Image",
|
||||||
|
"description": "A featured image in previews and on top of page",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"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')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,38 @@
|
||||||
{
|
{
|
||||||
"$schema": "/src/routes/sluzby/schema.json",
|
"title": "DALŠÍ VELMI OBLÍBENÉ SLUŽBY",
|
||||||
"category": "DALŠÍ VELMI OBLÍBENÉ SLUŽBY",
|
"description": "A description of a description",
|
||||||
"items": [
|
"id": "depilace",
|
||||||
|
"image": "",
|
||||||
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "Lifting řas booster (botox)",
|
"title": "Lifting řas booster (botox)",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "lifting-ras-booster",
|
"id": "lifting-ras-booster",
|
||||||
|
"image": "1,2,zola",
|
||||||
"price": 500,
|
"price": 500,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Laminace obočí + výživa",
|
"title": "Laminace obočí + výživa",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "laminace-oboci-vyziva",
|
"id": "laminace-oboci-vyziva",
|
||||||
|
"image": "",
|
||||||
"price": 500,
|
"price": 500,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Úprava obočí (tvar + barva)",
|
"title": "Úprava obočí (tvar + barva)",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "uprava-oboci-tvar-barva",
|
"id": "uprava-oboci-tvar-barva",
|
||||||
|
"image": "",
|
||||||
"price": 250,
|
"price": 250,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Úprava obočí + řasy (tvar + barvení)",
|
"title": "Úprava obočí + řasy (tvar + barvení)",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "uprava-oboci-rasy-tvar-barveni",
|
"id": "uprava-oboci-rasy-tvar-barveni",
|
||||||
|
"image": "",
|
||||||
"price": 300,
|
"price": 300,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
}
|
}
|
|
@ -1,67 +1,78 @@
|
||||||
{
|
{
|
||||||
"$schema": "/src/routes/sluzby/schema.json",
|
"title": "Depilace",
|
||||||
"category": "Depilace",
|
"description": "A description of a description",
|
||||||
"items": [
|
"id": "depilace",
|
||||||
|
"image": "",
|
||||||
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "Depilace Horní ret",
|
"title": "Depilace Horní ret",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-horni-ret",
|
"id": "depilace-horni-ret",
|
||||||
|
"image": "",
|
||||||
"price": 80,
|
"price": 80,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Brada",
|
"title": "Depilace Brada",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-brada",
|
"id": "depilace-brada",
|
||||||
|
"image": "",
|
||||||
"price": 80,
|
"price": 80,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Obočí",
|
"title": "Depilace Obočí",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-oboci",
|
"id": "depilace-oboci",
|
||||||
|
"image": "",
|
||||||
"price": 150,
|
"price": 150,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Tváře",
|
"title": "Depilace Tváře",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-tvare",
|
"id": "depilace-tvare",
|
||||||
|
"image": "",
|
||||||
"price": 150,
|
"price": 150,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Podpaží",
|
"title": "Depilace Podpaží",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-podpazi",
|
"id": "depilace-podpazi",
|
||||||
|
"image": "",
|
||||||
"price": 150,
|
"price": 150,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Předloktí",
|
"title": "Depilace Předloktí",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-predlokti",
|
"id": "depilace-predlokti",
|
||||||
|
"image": "",
|
||||||
"price": 200,
|
"price": 200,
|
||||||
"duration": 30
|
"duration": 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Celé ruce",
|
"title": "Depilace Celé ruce",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-cele-ruce",
|
"id": "depilace-cele-ruce",
|
||||||
|
"image": "",
|
||||||
"price": 350,
|
"price": 350,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Lýtka",
|
"title": "Depilace Lýtka",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-lytka",
|
"id": "depilace-lytka",
|
||||||
|
"image": "",
|
||||||
"price": 350,
|
"price": 350,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Depilace Celé nohy",
|
"title": "Depilace Celé nohy",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "depilace-cele-nohy",
|
"id": "depilace-cele-nohy",
|
||||||
|
"image": "",
|
||||||
"price": 500,
|
"price": 500,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
}
|
}
|
|
@ -1,46 +1,54 @@
|
||||||
{
|
{
|
||||||
"$schema": "/src/routes/sluzby/schema.json",
|
"title": "Kosmetické ošetření",
|
||||||
"category": "Kosmetické ošetření",
|
"description": "A description of a description",
|
||||||
"items": [
|
"id": "depilace",
|
||||||
|
"image": "",
|
||||||
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "ZÁKLADNÍ CALM",
|
"title": "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)",
|
"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",
|
"id": "zakladni-calm",
|
||||||
|
"image": "",
|
||||||
"price": 500,
|
"price": 500,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ZÁKLADNÍ + CALM PLUS",
|
"title": "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)",
|
"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",
|
"id": "zakladni-calm-plus",
|
||||||
|
"image": "",
|
||||||
"price": 600,
|
"price": 600,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "RELAXAČNÍ",
|
"title": "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)",
|
"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",
|
"id": "relaxacni",
|
||||||
|
"image": "",
|
||||||
"price": 690,
|
"price": 690,
|
||||||
"duration": 90
|
"duration": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LIFTINGOVÉ - ANTI AGE",
|
"title": "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)",
|
"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",
|
"id": "liftingove-anti-age",
|
||||||
|
"image": "",
|
||||||
"price": 690,
|
"price": 690,
|
||||||
"duration": 90
|
"duration": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CLEAR + ANTI AKNÉ",
|
"title": "CLEAR + ANTI AKNÉ",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "clear-anti-akne",
|
"id": "clear-anti-akne",
|
||||||
|
"image": "",
|
||||||
"price": 690,
|
"price": 690,
|
||||||
"duration": 90
|
"duration": 90
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Odlíčení + sérum + alginátová maska (PROJASNĚNÍ)",
|
"title": "Odlíčení + sérum + alginátová maska (PROJASNĚNÍ)",
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
"id": "odliceni-serum-alginatova-maska",
|
"id": "odliceni-serum-alginatova-maska",
|
||||||
|
"image": "",
|
||||||
"price": 300,
|
"price": 300,
|
||||||
"duration": 60
|
"duration": 60
|
||||||
}
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Kosmetické Ošetření
|
||||||
|
|
||||||
|
V mém salonu pracuji s profesionální bio kosmetikou Mesmerie.
|
||||||
|
KAŽDÉ OŠETŘENÍ NASTAVUJI NA DANÝ AKTUÁLNÍ STAV PLETI A CO JAKÁ ČÁST V OBLIČEJI POTŘEBUJE.
|
||||||
|
|
||||||
|
- Produkty jsou bez parabenů,parafínů a syntetické parfemace.
|
||||||
|
|
||||||
|
- Obsahuje velmi aktivní látky s vysokou koncentrací zpracované nanotechnologie.
|
||||||
|
|
||||||
|
- Zaručuje zdravou, vyživenou a krásnou pleť v každém věku.
|
||||||
|
|
||||||
|
- Zaměřuje se na PREVENCI x ŘEŠENÍ PROBLÉMU x OCHRANU
|
||||||
|
|
||||||
|
- Řeší příčinu vzniku problémů pleti a v kombinaci s kvalitním, profesionálním přístupem vaší terapeutky jsou rychlejší výsledky.
|
||||||
|
|
||||||
|
- Tato kosmetika se používá pro manuální ošetření tak i přístrojové
|
||||||
|
|
||||||
|
## Ošetření klasic obsahuje
|
||||||
|
|
||||||
|
- Povrchové odlíčení pleti a příprava k intenzivnějšímu ošetření
|
||||||
|
|
||||||
|
- Úprava, depilace, obočí, brada, horní ret, barvení obočí, řas
|
||||||
|
|
||||||
|
- Peeling (enzymaticky, mechanický dle stavu pleti)
|
||||||
|
|
||||||
|
- UZ špachtle (otevře komedomy, změkčení pleti)
|
||||||
|
|
||||||
|
- Mechanické ruční čištění pleti
|
||||||
|
|
||||||
|
- Aplikace aktivního séra
|
||||||
|
|
||||||
|
- Masáž relaxační/liftingová
|
||||||
|
|
||||||
|
- Maska k uzavření péče (multimásking)
|
||||||
|
|
||||||
|
- Závěrečná péče /krém)
|
||||||
|
|
||||||
|
## Ošetření speciál obsahuje
|
||||||
|
|
||||||
|
- Povrchové odlíčení pleti a příprava k intenzivnějšímu ošetření
|
||||||
|
|
||||||
|
- Úprava, depilace, obočí, brada, horní ret, barvení obočí, řas
|
||||||
|
|
||||||
|
- Lash lifting řas/laminace obočí
|
||||||
|
|
||||||
|
- Peeling (enzymaticky, mechanický dle stavu pleti)
|
||||||
|
|
||||||
|
- UZ špachtle (otevře komedomy, změkčení pleti)
|
||||||
|
|
||||||
|
- Mechanické čištění pleti ruční/přístrojem microdermabrazí
|
||||||
|
|
||||||
|
- Aplikace aktivního séra
|
||||||
|
|
||||||
|
- Masáž relaxační/liftingová
|
||||||
|
|
||||||
|
- Maska k uzavření péče (multimásking)
|
||||||
|
|
||||||
|
- Závěrečná péče /krém)
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: Mikrodermabraze
|
||||||
|
description: First post.
|
||||||
|
date: '2023-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Popis
|
||||||
|
|
||||||
|
Mikrodermabraze je velmi šetrnou a neinvazivní přístrojovou metodou na vyhlazení a omlazení pleti, která spolehlivě vyhlazuje a redukuje vrásky na obličeji a dekoltu. Dále umí redukovat i drobnější jizvičky a strie. Mikrodermabraze funguje na principu mechanického peelingu. Kdy pomocí speciálních krystalů a současně působení vakua dochází k postupnému odstranění odumřelých buněk a značnému zkvalitnění průchodnosti pokožky pro následnou aplikaci výživových sér. V průběhu mikrodermabraze působí navíc lymfatická drenáž, která příznivě stimuluje produkci kožního kolagenu, čistí se póry a zvyšuje se celkové prokrvení kůže. Účinky mikrodermabraze vedou postupně k novotvorbě kolagenních a elastinových vláken a tedy k žádanému vyhlazení a omlazení pokožky. Výsledky jsou viditelné již po prvním ošetření.
|
|
@ -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": 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Horní linky - meziřasové přirozené",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "linky",
|
|
||||||
"price": 2000,
|
|
||||||
"duration": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Klasické linky - s ocáskem",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "classic-linky",
|
|
||||||
"price": 3000,
|
|
||||||
"duration": 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Klasické linky - s ocáskem + spodní linky",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "classic-linky+spodni",
|
|
||||||
"price": 3500,
|
|
||||||
"duration": 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Rty - kontura",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "rty",
|
|
||||||
"price": 2500,
|
|
||||||
"duration": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "3D Rty (kontura a stínování), Full Lips (plné rty)",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "3d-rty",
|
|
||||||
"price": 3500,
|
|
||||||
"duration": 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Aquarelle Lips (přirodní stínování, bez kontury)",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "aquarelle",
|
|
||||||
"price": 3000,
|
|
||||||
"duration": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "První korekce po aplikaci pmu max. do 3 měsíců",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "korekce",
|
|
||||||
"price": 1000,
|
|
||||||
"duration": 90
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Oprava práce obočí jiného salonu",
|
|
||||||
"description": "Diagnostika pleti, odlíčení tonizace",
|
|
||||||
"id": "oprava-oboci",
|
|
||||||
"price": "na domluvě",
|
|
||||||
"duration": "na domluvě"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
title: Permanentní Make-up obočí
|
||||||
|
date: '2023-4-14'
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
## Meziřasová linka: Dokonale zvýrazněné řasy bez každodenního líčení
|
||||||
|
|
||||||
|
Toužíte po hustých a výraznějších řasách, ale každodenní aplikace řasenky vám zabírá příliš času? Permanentní make-up linek je pro vás ideálním řešením!
|
||||||
|
|
||||||
|
## Co jsou meziřasové linky ?
|
||||||
|
|
||||||
|
Linky meziřasové nejsou klasické černé linky. Jedná se o jemné tečky pigmentu aplikované speciálním strojkem přímo mezi vaše řasy. Tímto způsobem dochází k optickému **zhuštění řas** a zvýraznění jejich linie, aniž by výsledný efekt působil nepřirozeně.
|
||||||
|
|
||||||
|
## Proč si vybrat permanentní linky meziřasové?
|
||||||
|
|
||||||
|
- **Úspora času:** Už žádné každodenní nanášení řasenky. Permanentní meziřasové linky vám dodají efekt hustých řas po dobu 1,5-2 let.
|
||||||
|
- **Přirozený vzhled:** Jemné tečkování pigmentu vytváří realistický efekt hustších řas bez nutnosti výrazných linek.
|
||||||
|
- **Vhodné pro citlivé oči:** meziřasové linky jsou ideální volbou pro ženy s citlivýma očima, které nemohou používat klasickou řasenku.
|
||||||
|
- **Odolnost vůči vodě a rozmazání:** Permanentní linky meziřasové jsou voděodolné a nerozmažou se ani během cvičení nebo v horkém počasí.
|
||||||
|
- **Dlouhotrvající efekt:** Užijte si efekt hustých řas po dobu 1,5-2 let bez nutnosti častých korekcí.
|
||||||
|
|
||||||
|
## Jak probíhá procedura permanentního make-upu linek mezirasových?
|
||||||
|
|
||||||
|
- **Bezplatná konzultace:** Na konzultaci s certifikovanou kosmetičkou proberete svá přání a očekávání. Kosmetička vám vysvětlí celý proces a vybere vhodný odstín pigmentu.
|
||||||
|
- **Dezinfekce a nanesení anestetika:** Před samotným zákrokem probíhá důkladná dezinfekce očních víček a nanesení lokálního anestetika pro minimalizaci nepříjemných pocitů.
|
||||||
|
- **Aplikace pigmentu:** Zkušená kosmetička aplikuje vybraný pigment pomocí speciálního strojku do míst mezi vašimi řasami.
|
||||||
|
- **Hojení:** Po zákroku je třeba dodržovat doporučený postup péče o oční víčka, aby se pokožka rychle a správně zhojila.
|
||||||
|
|
||||||
|
## Permanentní make-up meziřasových linek je vhodný pro
|
||||||
|
|
||||||
|
- Chcete ušetřit čas strávený líčením řas
|
||||||
|
- Toužíte po přirozeně vypadajících hustších řasách
|
||||||
|
- Máte citlivé oči, nemůžete používat klasickou řasenku
|
||||||
|
- Máte aktivní životní styl, a oceníte voděodolnost a trvanlivost permanentního make-upu
|
||||||
|
|
||||||
|
**Toužíte po dokonale zvýrazněných řasách a neodolatelném pohledu?**
|
||||||
|
|
||||||
|
Kontaktujte mne a domluvte si bezplatnou konzultaci! Na konzultaci s vámi probereme vaše přání a očekávání a vybereme pro vás tu nejlepší techniku permanentního make-upu obočí.
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
title: Permanentní Make-up očních linek
|
||||||
|
description: First post.
|
||||||
|
date: '2023-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Obdarujte se dokonalými očními linkami
|
||||||
|
|
||||||
|
Permanentní make-up očních linek je moderní metoda, která vám pomůže ušetřit čas strávený líčením a zároveň zvýraznit krásu vašich očí. Díky této technice získáte perfektní a dlouhotrvající oční linky, které rámují vaše oči a dodávají jim neodolatelný vzhled.
|
||||||
|
|
||||||
|
## Jaké jsou výhody permanentního make-upu očních linek klasické?
|
||||||
|
|
||||||
|
- **Úspora času:** Už žádné každodenní kreslení linek tužkou nebo štětcem. Permanentní make-up vám dodá perfektní linky po dobu 2-3 let bez nutnosti neustálého líčení.
|
||||||
|
- **Dokonalý tvar a symetrie:** Odborná kosmetička vám navrhne a vytvoří tvar linek, který bude lichotit vašemu tvaru očí a případně korigovat drobné asymetrie.
|
||||||
|
- **Zvýraznění očí:** Oční linky zvýrazní vaše oči a dodají jim větší hloubku a výraz.
|
||||||
|
- Možnost výběru stylu: Můžete si vybrat z různých stylů linek, od jemných a nenápadných linek pro přirozený vzhled, až po výraznější linky pro dramatický efekt.
|
||||||
|
- **Dlouhotrvající efekt:** Užijte si perfektní oční linky po dobu 2-3 let bez nutnosti častých korekcí.
|
||||||
|
- **Sebedůvěra:** Permanentní make-up očních linek vám dodá pocit sebedůvěry a jistoty, že vaše oči vypadají vždycky skvěle.
|
||||||
|
|
||||||
|
## Jak probíhá procedura permanentního make-upu očních linek klasické?
|
||||||
|
|
||||||
|
- **Bezplatná konzultace:** Na konzultaci s certifikovanou kosmetičkou proberete svá přání a očekávání. Kosmetička vám navrhne vhodný styl linek a vybere odstín pigmentu tak, aby ladil s vaší barvou vlasů a pletí.
|
||||||
|
- **Dezinfekce a nanesení anestetika:** Před samotným zákrokem probíhá důkladná dezinfekce očních víček a nanesení lokálního anestetika pro minimalizaci nepříjemných pocitů.
|
||||||
|
- **Aplikace pigmentu:** Zkušená kosmetička aplikuje vybraný pigment pomocí speciálního strojku do nejvrchnější vrstvy pokožky očních víček.
|
||||||
|
- **Hojení:** Po zákroku je třeba dodržovat doporučený postup péče o oční víčka, aby se pokožka rychle a správně zhojila.
|
||||||
|
|
||||||
|
## Permanentní make-up očních linek klasické je vhodný když
|
||||||
|
|
||||||
|
- Chcete ušetřit čas strávený líčením očí
|
||||||
|
- Toužíte po dokonalém a dlouhotrvajícím tvaru očních linek
|
||||||
|
- Chcete zvýraznit své oči a dodat jim větší hloubku
|
||||||
|
- Máte alergie na produkty pro líčení očí
|
||||||
|
|
||||||
|
Toužíte po dokonalých očních linkách, které zvýrazní vaši krásu?
|
||||||
|
|
||||||
|
Kontaktujte mne a domluvte si bezplatnou konzultaci! Na konzultaci s vámi probereme vaše přání a očekávání a vybereme pro vás tu nejlepší techniku permanentního make-upu obočí.
|
|
@ -0,0 +1,40 @@
|
||||||
|
---
|
||||||
|
title: Permanentní Make-up obočí
|
||||||
|
description: First post.
|
||||||
|
date: '2023-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Získejte perfektní obočí, které podtrhne vaši krásu
|
||||||
|
|
||||||
|
Permanentní make-up obočí je moderní metoda, která vám pomůže ušetřit čas strávený líčením a zároveň dosáhnout perfektního tvaru a barvy obočí, které lichotí vašemu obličeji. Díky této technice budete mít neustále upravené a přirozeně vypadající obočí, aniž byste museli trávit čas jeho ranním kreslením.
|
||||||
|
|
||||||
|
## Jaké jsou výhody permanentního make-upu obočí?
|
||||||
|
|
||||||
|
- **Úspora času:** Už žádné každodenní kreslení obočí tužkou nebo štětcem. Permanentní make-up vám dodá perfektní obočí po dobu 2-3 let bez nutnosti neustálého líčení.
|
||||||
|
- **Přirozený vzhled:** Moderní techniky permanentního make-upu obočí se zaměřují na jemné stínování a individuální výběr odstínu tak, aby výsledek vypadal maximálně přirozeně a ladil s vaším typem pleti a barvou vlasů.
|
||||||
|
- **Dokonalý tvar:** Permanentní make-up dokáže upravit tvar obočí, korigovat jeho asymetrii a vyplnit prázdná místa mezi chloupky.
|
||||||
|
- **Dlouhotrvající efekt:** Užijte si perfektní obočí po dobu 2-3 let bez nutnosti častých korekcí.
|
||||||
|
- **Sebedůvěra:** Permanentní make-up obočí vám dodá pocit sebedůvěry a jistoty, že vaše obočí vypadá vždycky skvěle, ať už děláte cokoliv.
|
||||||
|
|
||||||
|
## Jak probíhá procedura permanentního make-upu obočí?
|
||||||
|
|
||||||
|
- **Bezplatná konzultace:** Na konzultaci s certifikovaným odborníkem proberete svá přání a očekávání. Kosmetička vám navrhne vhodnou techniku a vybere odstín pigmentu tak, aby ladil s vaším typem pleti a barvou vlasů.
|
||||||
|
- **Dezinfekce a nanesení anestetika:** Před samotným zákrokem probíhá důkladná dezinfekce obočí a nanesení lokálního anestetika pro minimalizaci nepříjemných pocitů.
|
||||||
|
- **Aplikace pigmentu:** Zkušená kosmetička aplikuje vybraný pigment pomocí speciálního strojku do nejvrchnější vrstvy pokožky obočí.
|
||||||
|
- **Hojení:** Po zákroku je třeba dodržovat doporučený postup péče o obočí, aby se pokožka rychle a správně zhojila.
|
||||||
|
|
||||||
|
## Permanentní make-up obočí je vhodný když
|
||||||
|
|
||||||
|
Chcete ušetřit čas strávený líčením obočí
|
||||||
|
Toužíte po přirozeně vypadajícím a perfektně tvarovaném obočí
|
||||||
|
Máte řídké nebo nevýrazné obočí
|
||||||
|
Chcete korigovat asymetrii obočí
|
||||||
|
Máte s alergie na produkty pro líčení obočí
|
||||||
|
|
||||||
|
Toužíte po perfektním obočí, které podtrhne vaši krásu?
|
||||||
|
|
||||||
|
Kontaktujte mne a domluvte si bezplatnou konzultaci! Na konzultaci s vámi probereme vaše přání a očekávání a vybereme pro vás tu nejlepší techniku permanentního make-upu obočí.
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"title": "Permanentní make-up",
|
||||||
|
"description": "A description of a description",
|
||||||
|
"id": "pmu",
|
||||||
|
"image": "https://images.unsplash.com/xyz",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"title": "Obočí Pudrové",
|
||||||
|
"description": "Správně upravené obočí dokáže obličej rozjasnit i omladit. Pokud je vaše obočí příliš světlé / řídké nebo vás prostě jen zdržuje každodenní úprava do požadovaného tvaru, nabízím vám velmi oblíbenou metodu permanentního make-upu – pudrové obočí – která je naprosto skvělým řešením!",
|
||||||
|
"id": "oboci",
|
||||||
|
"image": "",
|
||||||
|
"price": 3500,
|
||||||
|
"duration": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Meziřasové Linky",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "linky",
|
||||||
|
"image": "",
|
||||||
|
"price": 2500,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Klasické linky",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "classic-linky",
|
||||||
|
"image": "",
|
||||||
|
"price": 3000,
|
||||||
|
"duration": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Rty",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "rty",
|
||||||
|
"image": "",
|
||||||
|
"price": 2500,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Korekce",
|
||||||
|
"description": "Pro docílení perfektního vzhledu vašeho permanentního makeupu jsou potřeba dvě návštěvy. Ideální načasování pro druhou návštěvu je 1-3 měsíce od prvního zákroku.",
|
||||||
|
"id": "korekce",
|
||||||
|
"image": "",
|
||||||
|
"price": 1000,
|
||||||
|
"duration": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Oprava práce obočí jiného salonu",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "oprava-oboci",
|
||||||
|
"image": "",
|
||||||
|
"price": "na domluvě",
|
||||||
|
"duration": "na domluvě"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
title: Permanentní Make-up
|
||||||
|
description: First post.
|
||||||
|
date: '2023-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Popis
|
||||||
|
|
||||||
|
Permanentní make-up slouží jako náhrada dekorativní kosmetiky. Tento způsob zkrášlení ženám ulehčuje život, zkracuje čas strávený před zrcadlem a v neposlední řadě dodává sebevědomí. S upraveným obočím se cítí každá žena krásná za všech okolností. Dokáže korigovat nerovnosti obličeje, opticky omladit tvář nebo zamaskovat depigmentaci rtů. Správně provedená aplikace permanentního make-upu by neměla přinést výsledek na celý život. Omezená životnost je žádoucí zejména proto, že trendy v líčení se postupem let mění a žena by měla mít možnost svou vizáž obměnit. Jelikož je speciální barva na PMU vpravována do kůže podobně jako u tetování, nikdy se vám nestane, že by se například během sportu či koupání makeup rozmazal a stále perfektně drží.
|
||||||
|
|
||||||
|
## Co všechno se dá permanentně nalíčit?
|
||||||
|
|
||||||
|
Metodou permanentního make-upu si můžete nechat zvýraznit obočí, rty nebo oční linky. V našem studiu provádíme velké množství technik s použitím široké škály pigmentů, z kterých si vybere každá klientka.
|
||||||
|
|
||||||
|
## Korekce
|
||||||
|
|
||||||
|
Pro docílení perfektního vzhledu vašeho permanentního make-upu jsou často potřeba dvě návštěvy. V průběhu prvního sezení je do vrchní vrstvy kůže aplikován speciální pigment. Ačkoliv se jedná o velmi šetrnou a neinvazivní metodu, neeliminuje to však reakci vašeho těla v podobě lymfatického a imunitního systému, který začne pracovat a snažit se pigment z pokožky vyloučit. Odhadovaná ztráta pigmentu při tomto procesu činí zhruba 30-50%. Korekce se provádí do 6 týdnů po první aplikaci, kdy je pokožka zcela zahojená a odstín pigmentu se ustálil ve finální podobě. Během korekce se může upravit tvar, doplnit chybějící pigment nebo PMU lehce zvýraznit dle přání klientky. Ideální načasování pro druhou návštěvu je 1-2 měsíce od prvního zákroku. V případě, že klientka dorazí rok po první aplikaci, jedná se již o obnovu. Informace o cenách jednotlivých sezení naleznete v ceníku
|
||||||
|
|
||||||
|
### Absolutní kontraindikace
|
||||||
|
|
||||||
|
tzn. _okolnost nebo stav zákaznice vylučující některé postupy, výkony, aplikace_
|
||||||
|
|
||||||
|
1) Těhotenství
|
||||||
|
2) Kojení (doporučuje se počkat 2 měsíce po kojení, kvůli hormonálním změnám v těle)
|
||||||
|
3) tendence ke keloidním jizvám (nadměrný růst tkáně jizvy a okolí)
|
||||||
|
4) Epilepsie
|
||||||
|
5) Imunodeficience, onkologičtí pacienti v léčbě (aplikace je možná nejdříve 6 měsíců po léčbě)
|
||||||
|
6) Lymfedém
|
||||||
|
7) Léky proti srážlivosti krve
|
||||||
|
8) Onemocnění krevních a krvetvorných orgánů (leukémie apod.)
|
||||||
|
9) Užívání antibiotik (zákrok možný po 14 dnech od konce léčby)
|
||||||
|
10) Alergie (Je nutné artistce nahlásit jaké klientka má)
|
||||||
|
11) Diabetes mellitus (cukrovka) závislý na inzulínu
|
||||||
|
12) Velmi vysoký krevní tlak
|
||||||
|
13) Kožní choroby, které se projevují zejména v oblasti čela a obočí (Atopický ekzém, dermatitida, lupénka, Rosacea/růžovka, Herpes
|
||||||
|
14) Hepatitida A, B, C
|
||||||
|
15) AIDS
|
||||||
|
16) Vrozené oslabení imunity (pokud ano, které?)
|
||||||
|
17) Autoimunní onemocnění
|
||||||
|
18) Pohlavní nemoci
|
||||||
|
19) Akutní infekce s teplotami
|
||||||
|
20) Léky na štítnou žlázu (je třeba konzultace)
|
||||||
|
21) Systémová autoimunitní onemocnění (revmatismus a revmatoidní artritida, systémový lupus (erythematosus), vaskulitida, sklerodermie atd.)
|
||||||
|
|
||||||
|
### Relativní kontraindikace
|
||||||
|
|
||||||
|
tzn. _možné, dočasné kontraindikace_
|
||||||
|
|
||||||
|
1) Zdravotní potíže jakéhokoliv původu
|
||||||
|
2) ARVI (akutní respirační virové infekce)
|
||||||
|
3) dokonce i „jen rýma“ bez horečky – je lepší odložit permanentní make-up až do úplného zotavení.
|
||||||
|
4) Zanícená kůže, zejména v dotčené oblasti – Aktivní akné
|
||||||
|
5) Větší lékařské zásahy a medikace (kortikosteroidy, antikoagulancia, retinoidy atd.) při radioterapii a chemoterapii. Procedura PMU může být proveden nejméně 2 týdny po dokončení léčby (nebo později, v závislosti na léku a stavu zákazníka).
|
||||||
|
6) Benigní novotvary kůže v oblasti zásahu – mikropigmentace mateřských znamének je zakázána, protože nesmí být traumatizována jehlou; verruca (bradavice) a papilomy – po provedení perm. make- upu se může změnit jejich velikost nebo zvýšit výsev, proto je nutné je předem odstranit.
|
||||||
|
7) Autoimunitní onemocnění mají následující znaky: lidské tělo začíná vytvářet protilátky proti vlastním buňkám, tj. imunitní buňky vnímají normální buňky téhož těla jako cizorodé a snaží se je likvidovat. Trvalý make-up může být proveden pouze při remisi onemocnění a za písemného souhlasu poskytovatele zdravotní péče.
|
||||||
|
8) Herpes představuje kontraindikaci proti jakémukoli typu trvalého make-upu. Permanentní make-up se může provést až po úplném vymizení projevů nemoci za 3-4 týdny a při pigmentaci rtů jedině po preventivním podání antivirového léku.
|
||||||
|
9) Konjunktivitida (zánět spojivek) – v případě alergické konjunktivitidy může permanentní make-up stav očí zlepšit, protože se po tomto postupu eliminuje dráždění kosmetickými přípravky. Avšak v obou případech alergické i bakteriální konjunktivitidy může být permanentní make-up prováděn pouze v neaktivní fázi.
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
title: "Permanentní make-up rtů: Pro krásné a sebevědomé rty"
|
||||||
|
description: First post.
|
||||||
|
date: '2024-4-14'
|
||||||
|
categories:
|
||||||
|
- sveltekit
|
||||||
|
- svelte
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
## Získejte dokonalé rty, které rozzáří váš úsměv a dodají vám sebedůvěru
|
||||||
|
|
||||||
|
PMU rtů je moderní metoda, která vám pomůže ušetřit čas strávený líčením a zároveň zvýraznit krásu vašich rtů. Díky této technice můžete mít neustále upravené a přirozeně vypadající rty, ať už ráno po probuzení, během náročného dne nebo při sportovních aktivitách.
|
||||||
|
|
||||||
|
## Jaké jsou výhody ?
|
||||||
|
|
||||||
|
- **Úspora času:** Už žádné každodenní malování rtěnkou. Permanentní make-up vám dodá dokonalé rty po dobu 2-3 let bez nutnosti neustálého líčení.
|
||||||
|
- **Zvýraznění rtů:** Permanentní make-up dokáže opticky zvětšit nebo zmenšit rty, korigovat jejich asymetrii a zvýraznit jejich kontury, zakrýt nedostatky.
|
||||||
|
- **Přirozený vzhled:** Moderní techniky permanentního make-upu rtů se zaměřují na jemné stínování a individuální výběr odstínu tak, aby výsledek vypadal maximálně přirozeně a ladil s vaším typem pleti a barvou vlasů.
|
||||||
|
- **Sebedůvěra:** Permanentní make-up rtů vám dodá pocit sebedůvěry a jistoty, že vaše rty vypadají vždycky skvěle, ať už děláte cokoliv.
|
||||||
|
- **Dlouhotrvající efekt:** Užijte si dokonale nalíčené rty po dobu 2-3 let bez nutnosti častých korekcí
|
||||||
|
|
||||||
|
## Techniky permanentního make-upu rtů
|
||||||
|
|
||||||
|
- **Kontury rtů:** Tato metoda doplní obrys rtu a dodá mu definici.
|
||||||
|
- **Aquarel lips:** Tato technika stínování rtů směrem od okraje dovnitř vytváří 3D efekt a opticky zvětšuje rty.
|
||||||
|
- **Superbright lips:** Tato technika rovnoměrného probarvení rtů s hustě prosyceným pigmentem je ideální pro klientky, které chtějí výrazné rty.
|
||||||
|
|
||||||
|
## Co vás čeká?
|
||||||
|
|
||||||
|
1. **Bezplatná konzultace:** Na konzultaci s certifikovaným odborníkem proberete svá přání a očekávání. Kosmetička vám navrhne vhodnou techniku a vybere odstín pigmentu tak, aby ladil s vaším typem pleti a rtů.
|
||||||
|
2. **Dezinfekce a nanesení anestetika:** Před samotným zákrokem probíhá důkladná dezinfekce rtů a nanesení lokálního anestetika pro minimalizaci nepříjemných pocitů.
|
||||||
|
3. **Aplikace pigmentu:** Zkušená kosmetička aplikuje vybraný pigment pomocí speciálního strojku do nejvrchnější vrstvy pokožky rtů.
|
||||||
|
4. **Hojení:** Po zákroku je třeba dodržovat doporučený postup péče o rty, aby se pokožka rychle a správně zhojila
|
||||||
|
5. Dále naplánujeme korekturu a můžete se těšit na krásné a bezchybné rty po celé 2-3 roky!
|
||||||
|
|
||||||
|
## Permanentní make-up rtů je ideální když
|
||||||
|
|
||||||
|
- Chcete ušetřit čas strávený líčením.
|
||||||
|
- Toužíte po přirozeně vypadajících a upravených rtech po celý den.
|
||||||
|
- Jestli trpíte alergiemi na rtěnky.
|
||||||
|
- Máte nevýrazné rty.
|
||||||
|
- Chcete zakrýt nedostatky rtů.
|
||||||
|
|
||||||
|
Investujte do sebe a dopřejte si dokonalé rty s permanentním make-upem!
|
||||||
|
|
||||||
|
Pro více informací a objednání se mě neváhejte kontaktovat.
|
|
@ -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": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": 120
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"title": "Vakuslim 48 - zeštíhlující procedura",
|
||||||
|
"description": "A description of a description",
|
||||||
|
"id": "depilace",
|
||||||
|
"image": "",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"title": "Ošetření horních končetin",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "horni-koncetiny",
|
||||||
|
"image": "",
|
||||||
|
"price": 600,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "1 ošetření spodní části těla (břicho, boky, dolní končetiny)",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "spodni-cast-tela",
|
||||||
|
"image": "",
|
||||||
|
"price": 800,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "1 ošetření komplet horní-dolní části",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "komplet-horni-dolni-cast",
|
||||||
|
"image": "",
|
||||||
|
"price": 1200,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "6 ošetření předplatné komplet",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "6-o-setreni-predplatne-kompet",
|
||||||
|
"image": "",
|
||||||
|
"price": 6600,
|
||||||
|
"duration": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "12 ošetření předplatné komplet",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "12-o-setreni-predplatne-komplet",
|
||||||
|
"image": "",
|
||||||
|
"price": 11000,
|
||||||
|
"duration": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Svatebni Liceni
|
||||||
|
|
||||||
|
Vytvořím Vám jemné, stylové líčení na svatbu a nemusíte se bát ani slziček, protože voděodolný make-up vydrží naprosto vše (a na pohled bude stále decentní), ale přesto dostatečně výrazný jak na fotkách, tak na videu. Cílem mé práce je, aby pleť vypadala přirozeně a svěže po celý svatební den.
|
||||||
|
|
||||||
|
Abyste se ve svůj den D cítila krásně, je třeba začít se správnou péčí o pleť minimálně 3 měsíce před svatbou. Pokud péči o pleť nebudete věnovat pozornost, ani dokonalý svatební make-up vám nezaručí vysněný vzhled, ba naopak, může i uškodit a vypadat nepřirozeně.
|
||||||
|
|
||||||
|
## KROKY PŘÍPRAVY
|
||||||
|
|
||||||
|
Nevěsta by měla absolvovat minimálně jednu zkoušku, která by měla proběhnout nejpozději týden před obřadem.
|
||||||
|
|
||||||
|
Nejdříve VÁM zhodnotím typ pleti, tvar obličeje, tvar očí, úst a následně doporučím a vyzkoušíme make-up přímo na míru. Někomu sluší spíš takzvaný „nude“ a někomu zase naopak výraznější líčení. Díky této zkoušce klientka bude vědět v jakém make-upu se cítí nejlépe a doladí se všechny detaily.
|
||||||
|
|
||||||
|
Milé budoucí nevěsty, přineste si s sebou na fotce také svatební šaty a kytici. Je totiž důležité, aby make-up ladil s celkovým stylem a barvou svatby.
|
||||||
|
|
||||||
|
## PRŮBĚH V DEN D (SVATEBNÍ DEN)
|
||||||
|
|
||||||
|
Ve svatební den nejprve pleť správně připravíme pomocí kosmetických přípravků (vyčištění, hydratace, tonikum, hydratační sérum, pleťový krém) dále make-upu, zakryje drobné nedokonalosti korektorem, přepudruje se pleť a pomocí konturování podtrhne Vaší krásu. Dále pomocí stínů zvýrazní oči a pokud má nevěsta zájem, lze nalepit i řasy. Na závěr se vybere vhodný odstín rtěnky, make-up se zafixuje a vy si můžete jít naplno užít Váš den.
|
||||||
|
|
||||||
|
## Cenik svatebního líčení
|
||||||
|
|
||||||
|
Zkouška svatebního líčení trvá přibližně 60-90 minut a stojí 990 Kč.
|
||||||
|
|
||||||
|
Zkouška účesu 300,-
|
||||||
|
|
||||||
|
Svatební den líčení 1500,-
|
||||||
|
|
||||||
|
Svatební den účes 400,-
|
||||||
|
|
||||||
|
Možná domluva prodloužení a zahuštění vlasů metodou keratin (pásky) cena se odvíjí od hustoty (používám pásky 100% human hair, levnější varianta 60-80% mix lidské+sintet. Potřebná domluva předem.
|
||||||
|
|
||||||
|
Ceník human hair 100% 20pásků 3500,-
|
||||||
|
|
||||||
|
Ceník human+syntetika 20pásků 1500,-
|
||||||
|
|
||||||
|
Ceník sundání pramenů do 30min 350,-
|
|
@ -0,0 +1,17 @@
|
||||||
|
# VEČERNÍ + PLESOVÉ LÍČENÍ
|
||||||
|
|
||||||
|
Zahrnuje: báze, korektory, velká modelace obličeje, make-up, pudr, stíny, linky, řasy, obočí, líčka, rozjasňovače, fixátor a aplikace umelých řas (trvanlivost na noc), popř. účes.
|
||||||
|
|
||||||
|
60-90min. 990,-
|
||||||
|
|
||||||
|
120min. líčení + účes 1500,-
|
||||||
|
|
||||||
|
## Prodloužení vlasů
|
||||||
|
|
||||||
|
Možná domluva prodloužení a zahuštění vlasů metodou keratin (pásky) cena se odvíjí od hustoty (používám pásky 100% human hair, levnější varianta 60-80% mix lidské+syntet. Potřebná domluva předem.
|
||||||
|
|
||||||
|
Ceník human hair 100% 20pásků 4500,-
|
||||||
|
|
||||||
|
Ceník human+syntetika 20pásků 1500,-
|
||||||
|
|
||||||
|
Ceník sundání pramenů do 30min 350,-
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"title": "Vizážistika",
|
||||||
|
"description": "Nabízím Vám líčení a účes současně, ušetřete svůj čas o svatebním dni :-), večerní a plesové líčení, základní denní líčení.",
|
||||||
|
"id": "vizazistika",
|
||||||
|
"image": "https://images.unsplash.com/xyz",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"title": "Svatební Líčení",
|
||||||
|
"description": "Vytvořím Vám jemné, stylové líčení na svatbu a nemusíte se bát ani slziček, protože voděodolný make-up vydrží naprosto vše (a na pohled bude stále decentní), ale přesto dostatečně výrazný jak na fotkách, tak na videu. Cílem mé práce je, aby pleť vypadala přirozeně a svěže po celý svatební den.",
|
||||||
|
"id": "svatebni",
|
||||||
|
"image": "oboci1.png",
|
||||||
|
"price": 3500,
|
||||||
|
"duration": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Večerní & Plesové Líčení",
|
||||||
|
"description": "Diagnostika pleti, odlíčení tonizace",
|
||||||
|
"id": "vecerni",
|
||||||
|
"image": "",
|
||||||
|
"price": 2500,
|
||||||
|
"duration": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
# VIZÁŽISTIKA
|
||||||
|
|
||||||
|
Nabízím Vám líčení a účes současně, ušetřete svůj čas o svatebním dni :-), večerní a plesové líčení, základní denní líčení.
|
||||||
|
|
||||||
|
Práci vizážistky se věnuji po absolvování profesionální školy Make- up institute. Mám osobitý přístup a Vaše spokojenost mě motivuje k lepším výsledkům. Kromě služeb vizážistiky si Vás mohu připravit předem i kosmetickým ošetřením, do salonu mi dojíždí i manikérka. Spolupracuji s profesionální fotografkou, možná domluva focení.
|
|
@ -1,8 +1,13 @@
|
||||||
import { handleErrorWithSentry, replayIntegration } from "@sentry/sveltekit";
|
import { handleErrorWithSentry, replayIntegration } from "@sentry/sveltekit";
|
||||||
import * as Sentry from '@sentry/sveltekit';
|
import * as Sentry from '@sentry/sveltekit';
|
||||||
|
import {
|
||||||
|
PUBLIC_SENTRY_KEY,
|
||||||
|
PUBLIC_SENTRY_PROJECT_ID,
|
||||||
|
PUBLIC_SENTRY_ORG_ID
|
||||||
|
} from '$env/static/public';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://962a7ed3891a335e112746e5c6c6bf42@o4505828687478784.ingest.us.sentry.io/4506871754326016',
|
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
|
|
||||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
@ -14,11 +19,7 @@ Sentry.init({
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
// If you don't want to use Session Replay, just remove the line below:
|
// If you don't want to use Session Replay, just remove the line below:
|
||||||
integrations: [replayIntegration({
|
integrations: [replayIntegration()],
|
||||||
maskAllText: false,
|
|
||||||
blockAllMedia: false,
|
|
||||||
})
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { sequence } from "@sveltejs/kit/hooks";
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit";
|
|
||||||
|
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
|
||||||
import * as Sentry from '@sentry/sveltekit';
|
import * as Sentry from '@sentry/sveltekit';
|
||||||
import {
|
import {
|
||||||
PUBLIC_SENTRY_KEY,
|
PUBLIC_SENTRY_KEY,
|
||||||
|
@ -10,9 +11,12 @@ import {
|
||||||
|
|
||||||
import { csp, rootDomain } from './cspDirectives';
|
import { csp, rootDomain } from './cspDirectives';
|
||||||
|
|
||||||
|
import * as amp from '@sveltejs/amp';
|
||||||
|
import dropcss from 'dropcss';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://962a7ed3891a335e112746e5c6c6bf42@o4505828687478784.ingest.us.sentry.io/4506871754326016',
|
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cspHandle: Handle = async ({ event, resolve }) => {
|
export const cspHandle: Handle = async ({ event, resolve }) => {
|
||||||
|
@ -31,17 +35,43 @@ export const cspHandle: Handle = async ({ event, resolve }) => {
|
||||||
'Content-Security-Policy': csp,
|
'Content-Security-Policy': csp,
|
||||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
|
'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}"`,
|
'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}"}]}`,
|
'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}"}]}`
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(headers).forEach(([key, value]) => {
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
response.headers.set(key, value);
|
response.headers.set(key, value);
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ampHandle: Handle = async ({ event, resolve }) => {
|
||||||
|
let buffer = '';
|
||||||
|
return await resolve(event, {
|
||||||
|
transformPageChunk: ({ html, done }) => {
|
||||||
|
buffer += html;
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
let css = '';
|
||||||
|
const markup = amp
|
||||||
|
.transform(buffer)
|
||||||
|
.replace('⚡', 'amp') // dropcss can't handle this character
|
||||||
|
.replace(
|
||||||
|
/<style amp-custom([^>]*?)>([^]+?)<\/style>/,
|
||||||
|
(match, attributes, contents) => {
|
||||||
|
css = contents;
|
||||||
|
return `<style amp-custom${attributes}></style>`;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
css = dropcss({ css, html: markup }).css;
|
||||||
|
return markup.replace('</style>', `${css}</style>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
|
// 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: Handle = sequence(sentryHandle(), cspHandle, ampHandle);
|
||||||
|
|
||||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||||
export const handleError = handleErrorWithSentry();
|
export const handleError = handleErrorWithSentry();
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as conf from '$lib/config'
|
||||||
|
</script>
|
||||||
|
<section>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
lang="en-GB"
|
||||||
|
aria-labelledby="title"
|
||||||
|
viewBox="0 0 200 24">
|
||||||
|
|
||||||
|
<title id="title">Zašlete mi mail!</title>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<a href="mailto:{conf.socialLinks[3].title}" aria-label="Zašlete mi mail!">
|
||||||
|
<rect />
|
||||||
|
<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle">{conf.socialLinks[3].title}</text>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
rect {
|
||||||
|
width: 200px;
|
||||||
|
height: 24px;
|
||||||
|
fill: rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus rect,
|
||||||
|
rect:hover {
|
||||||
|
fill: indigo
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 16px;
|
||||||
|
fill: indigo;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus text,
|
||||||
|
rect:hover + text {
|
||||||
|
fill: rgb(255, 255, 255);
|
||||||
|
font-weight: 900;
|
||||||
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
|
||||||
|
text-decoration: underline 1px solid rgb(255, 255, 255);
|
||||||
|
text-underline-offset: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,13 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AppBar, LightSwitch } from '@skeletonlabs/skeleton';
|
import { AppBar, LightSwitch } from '@skeletonlabs/skeleton';
|
||||||
import * as config from '$lib/config';
|
import * as conf from '$lib/config';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppBar>
|
<AppBar>
|
||||||
<!-- Left-side Header -->
|
<!-- Left-side Header -->
|
||||||
<svelte:fragment slot="lead">
|
<svelte:fragment slot="lead">
|
||||||
<a class="lg:!ml-0 w-8 lg:w-auto overflow-hidden" href="/" title="Logo {config.title}">
|
<a class="lg:!ml-0 w-8 lg:w-auto overflow-hidden" href="/" title="Logo {conf.title}">
|
||||||
<Logo clazz="w-[325px] h-8 md:h-16"/>
|
<Logo clazz="w-[325px] h-8 md:h-16"/>
|
||||||
</a>
|
</a>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<section class=" sm:inline-flex space-x-1">
|
<section class=" sm:inline-flex space-x-1">
|
||||||
<a
|
<a
|
||||||
class="btn-icon hover:variant-soft-primary"
|
class="btn-icon hover:variant-soft-primary"
|
||||||
href="{config.instagram}"
|
href="{conf.socialLinks[0].href}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="btn-icon hover:variant-soft-primary"
|
class="btn-icon hover:variant-soft-primary"
|
||||||
href="{config.facebook}"
|
href="{conf.socialLinks[2].href}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
import { MapLibre, Marker, Popup } from 'svelte-maplibre';
|
import { MapLibre, Marker, Popup } from 'svelte-maplibre';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
|
|
||||||
|
import * as conf from '$lib/config';
|
||||||
|
import EmailObfuscated from './EmailObfuscated.svelte';
|
||||||
|
export let clazz = 'absolute inset-0';
|
||||||
|
|
||||||
export let lngLat = { lng: 49.317881, lat: 14.104978 };
|
export let lngLat = { lng: 49.317881, lat: 14.104978 };
|
||||||
export let clazz = "absolute inset-0"
|
|
||||||
let hasWebGL = false;
|
let hasWebGL = false;
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
hasWebGL = !!(window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
|
hasWebGL = !!(
|
||||||
|
window.WebGLRenderingContext &&
|
||||||
|
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hasWebGL = false;
|
hasWebGL = false;
|
||||||
}
|
}
|
||||||
|
@ -19,10 +28,18 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !browser}
|
{#if !browser}
|
||||||
<div>Pokud chcete zobrazit mapu, zvažte použití prohlížeče, pokud jste bot či scraper, jděte se vycpat.</div>
|
<div>
|
||||||
|
Pokud chcete zobrazit mapu, zvažte použití prohlížeče, pokud jste bot či scraper, jděte se
|
||||||
|
vycpat.
|
||||||
|
</div>
|
||||||
{:else if isLoading}
|
{:else if isLoading}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if hasWebGL}
|
{:else if hasWebGL}
|
||||||
|
<section class="relative">
|
||||||
|
<div class="container px-5 py-24 mx-auto flex sm:flex-nowrap flex-wrap">
|
||||||
|
<div
|
||||||
|
class="lg:w-2/3 md:w-1/2 bg-gray-300 rounded-lg overflow-hidden sm:mr-10 p-10 flex items-end justify-start relative"
|
||||||
|
>
|
||||||
<MapLibre
|
<MapLibre
|
||||||
center={[49.317881, 14.104978]}
|
center={[49.317881, 14.104978]}
|
||||||
zoom={2}
|
zoom={2}
|
||||||
|
@ -34,16 +51,33 @@
|
||||||
{lngLat}
|
{lngLat}
|
||||||
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-red-300 text-black shadow-2xl focus:outline-2 focus:outline-black"
|
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-red-300 text-black shadow-2xl focus:outline-2 focus:outline-black"
|
||||||
>
|
>
|
||||||
<span>
|
<span> KKosmetickySalon Oldřichov </span>
|
||||||
KKosmetickySalon Oldřichov
|
|
||||||
</span>
|
|
||||||
<Popup openOn="hover" offset={[0, -10]}>
|
<Popup openOn="hover" offset={[0, -10]}>
|
||||||
<div class="text-lg font-bold">Přijeďte ke mně :)</div>
|
<div class="text-lg font-bold">Přijeďte ke mně :)</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
</MapLibre>
|
</MapLibre>
|
||||||
|
<div class="bg-white relative flex flex-wrap py-6 rounded shadow-md">
|
||||||
|
<div class="lg:w-1/2 px-6">
|
||||||
|
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs">Adresa</h2>
|
||||||
|
<p class="mt-1">{conf.address}</p>
|
||||||
|
</div>
|
||||||
|
<div class="lg:w-1/2 px-6 mt-4 lg:mt-0">
|
||||||
|
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs">Email</h2>
|
||||||
|
<EmailObfuscated />
|
||||||
|
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs mt-4">
|
||||||
|
{conf.socialLinks[1].title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="map">Omlouváme se, tato funkce vyžaduje WebGL, pro zobrazení map na tomto webu ji prosím povolte.</div>
|
<div class="map">
|
||||||
|
Omlouváme se, mapy na tomto webu vyžadují funkci WebGL, pro zobrazení map ji v prohlížeči
|
||||||
|
povolte.
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
<!-- <script lang="ts">
|
||||||
|
import * as conf from '$lib/config'
|
||||||
|
import { getImageLink } from '$lib/images';
|
||||||
|
import type { ExtendedService, ExtendedCategory } from '$lib/types/service';
|
||||||
|
export let data: ExtendedService | ExtendedCategory
|
||||||
|
|
||||||
|
let seoData: ExtendedService | ExtendedCategory
|
||||||
|
|
||||||
|
export let title: string;
|
||||||
|
export let description: string;
|
||||||
|
export let type: string;
|
||||||
|
export let keywords: string;
|
||||||
|
export let image: string;
|
||||||
|
export let canonical: string;
|
||||||
|
export let twitter: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
} = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.title}</title>
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<meta name="googlebot" content="index,follow" />
|
||||||
|
{#if data.description}
|
||||||
|
<meta name="description" content={data.description} />
|
||||||
|
{/if}
|
||||||
|
<meta name="keywords" content={keywords} />
|
||||||
|
{#if data.canonical}
|
||||||
|
<link rel="canonical" href={data.canonical} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<meta property="og:site_name" content="{conf.title}" />
|
||||||
|
<meta property="og:title" content={data.title} />
|
||||||
|
<meta property="og:type" content={data.type ? data.type : 'site'} />
|
||||||
|
{#if data.description}
|
||||||
|
<meta property="og:description" content={data.description} />
|
||||||
|
{/if}
|
||||||
|
{#if data.canonical}
|
||||||
|
<meta property="og:url" content={data.canonical} />
|
||||||
|
{/if}
|
||||||
|
<meta property="og:image" content={getImageLink({id: data.id, h:, w:})} />
|
||||||
|
|
||||||
|
<meta property="fb:admins" content="${conf.FBNumID}" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content={data.title} />
|
||||||
|
{#if data.description}
|
||||||
|
<meta name="twitter:description" content={data.description} />
|
||||||
|
{/if}
|
||||||
|
<meta name="twitter:image" content={twitter.image || image} />
|
||||||
|
|
||||||
|
<meta property="article:published_time" content={data.frontmatter?.date} />
|
||||||
|
{#each data.frontmatter?.tags as tag (tag)}
|
||||||
|
<meta property="article:tag" content={tag} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</svelte:head> -->
|
||||||
|
<!-- src/lib/components/SEO.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import * as conf from '$lib/config';
|
||||||
|
|
||||||
|
export let seoData: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
keywords: string;
|
||||||
|
image: string;
|
||||||
|
canonical?: string;
|
||||||
|
twitter?: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
};
|
||||||
|
frontmatter?: {
|
||||||
|
date: string;
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$: title = seoData.title;
|
||||||
|
$: description = seoData.description;
|
||||||
|
$: type = seoData.type;
|
||||||
|
$: keywords = seoData.keywords;
|
||||||
|
$: image = seoData.image;
|
||||||
|
$: canonical = seoData.canonical;
|
||||||
|
$: twitter = seoData.twitter;
|
||||||
|
$: frontmatter = seoData.frontmatter;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title}</title>
|
||||||
|
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
|
||||||
|
<meta name="googlebot" content="index,follow" />
|
||||||
|
|
||||||
|
{#if description}
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<meta name="keywords" content={keywords} />
|
||||||
|
|
||||||
|
{#if canonical}
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<meta property="og:site_name" content={conf.title} />
|
||||||
|
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
|
||||||
|
{#if description}
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if canonical}
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<meta name="twitter:title" content={twitter?.title || title} />
|
||||||
|
|
||||||
|
{#if twitter?.description || description}
|
||||||
|
<meta name="twitter:description" content={twitter?.description || description} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<meta name="twitter:image" content={twitter?.image || image} />
|
||||||
|
|
||||||
|
<!--If there is frontmatter, make it into an article-->
|
||||||
|
{#if frontmatter}
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
{:else}
|
||||||
|
<meta property="og:type" content={type || 'website'} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if frontmatter?.date}
|
||||||
|
<meta property="article:published_time" content={frontmatter.date} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if frontmatter?.tags}
|
||||||
|
{#each frontmatter.tags as tag}
|
||||||
|
<meta property="article:tag" content={tag} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
|
@ -1,12 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type Product
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<container class="container">
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<img class="w-max h-max" src=`${product.image}` alt=`${product.name}`>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</container>
|
|
|
@ -3,26 +3,27 @@
|
||||||
import type { Service } from '$lib/types/service';
|
import type { Service } from '$lib/types/service';
|
||||||
import convertMinutesToHours from '$lib/utils/minToH';
|
import convertMinutesToHours from '$lib/utils/minToH';
|
||||||
|
|
||||||
export let item: Service['items'][number];
|
export let service: Service[][number];
|
||||||
|
|
||||||
</script>
|
</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/{service.id}" id="{service.id}"class="w-72 card variant-glass-secondary mx-2 my-4 duration-500 hover:scale-105 hover:shadow-xl shadow-md">
|
||||||
<header>
|
<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" />
|
// getImageLink maps all image links based on the services id, needs cloudinary implementation to work.
|
||||||
|
<!-- <img src={getImageLink({id: service.id, w: 288, h: 320 })} class="bg-black/50 object-cover aspect-[9/10] rounded-t-xl" alt="Post" loading="lazy" /> -->
|
||||||
</header>
|
</header>
|
||||||
<!-- <div class="img" style="background-image: url('/images/services/{item.id}.jpg');"/> -->
|
<!-- <div class="img" style="background-image: url('/images/services/{service.id}.jpg');"/> -->
|
||||||
<div class="flex flex-col px-4 py-3 w-72 h-full">
|
<div class="flex flex-col px-4 py-3 w-72 h-full">
|
||||||
<div class="flex flex-row justify-between px-2 gap-y-2">
|
<div class="flex flex-row justify-between px-2 gap-y-2">
|
||||||
<h3 class="h3 font-semibold w-full">{item.name}</h3>
|
<h3 class="h3 font-semibold w-full">{service.title}</h3>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
<p class="text-gray-800 font-medium hidden md:block mb-4">
|
<p class="text-gray-800 font-medium hidden md:block mb-4">
|
||||||
{item.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/sluzby/{item.id}"
|
href="/sluzby/{service.id}"
|
||||||
class="text-primary-600 hover:text-primary-800 underline mb-2 md:mb-0">
|
class="text-primary-600 hover:text-primary-800 underline mb-2 md:mb-0">
|
||||||
...Zjistěte více
|
...Zjistěte více
|
||||||
</a>
|
</a>
|
||||||
|
@ -32,11 +33,11 @@
|
||||||
<footer class="justify-self-end align-bottom">
|
<footer class="justify-self-end align-bottom">
|
||||||
<div class="flex flex-col md:flex-row md:justify-between items-center">
|
<div class="flex flex-col md:flex-row md:justify-between items-center">
|
||||||
<p class=" text-lg text-surface-900 font-semibold text-right">
|
<p class=" text-lg text-surface-900 font-semibold text-right">
|
||||||
{typeof item.price === 'number' ? `${item.price},-` : item.price}
|
{typeof service.price === 'number' ? `${service.price},-` : service.price}
|
||||||
{typeof item.duration === 'number' ? `/${convertMinutesToHours(item.duration)}` : ''}
|
{typeof service.duration === 'number' ? `/${convertMinutesToHours(service.duration)}` : ''}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="https://app.cal.com/kkosmetickysalon/{item.id}"
|
href="https://app.cal.com/kkosmetickysalon/{service.id}"
|
||||||
class="btn btn-md variant-filled-primary">
|
class="btn btn-md variant-filled-primary">
|
||||||
Rezervace
|
Rezervace
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,43 +1,30 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Service } from '$lib/types/service';
|
import type { Category } from '$lib/types/service';
|
||||||
import ServiceCard from '$lib/components/services/ServiceCard.svelte';
|
import ServiceCard from '$lib/components/services/ServiceCard.svelte';
|
||||||
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
|
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
|
||||||
|
|
||||||
export let services: Service[] = [];
|
export let category: Category[] = [];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
<Accordion class="bg-surface-100 w-full md:card p-4 md:max-w-[75%]">
|
<Accordion class="bg-surface-100 w-full md:card p-4 md:max-w-[75%]">
|
||||||
{#each services as service}
|
{#each category as c}
|
||||||
<AccordionItem on>
|
<AccordionItem on>
|
||||||
<svelte:fragment slot="lead">
|
<svelte:fragment slot="lead">
|
||||||
<i class="fa-solid fa-wand-magic-sparkles text-xl w-6 text-center" />
|
<i class="fa-solid fa-wand-magic-sparkles text-xl w-6 text-center" />
|
||||||
|
<h2 class="h2 text-bold">{c.title}</h2>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="summary">
|
<svelte:fragment slot="summary">
|
||||||
<p class="text-lg font-bold">{service.category}</p>
|
<p class="text-base">{c.description}</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
<div class="w-fit mx-auto flex flex-wrap justify-center justify-items-center gap-y-6 gap-x-8 mt-8 mb-5">
|
<div class="w-fit mx-auto flex flex-wrap justify-center justify-items-center gap-y-6 gap-x-8 mt-8 mb-5">
|
||||||
{#each service.items as item}
|
{#each c.services as service}
|
||||||
<ServiceCard {item} />
|
<ServiceCard {service} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
{/each}
|
{/each}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
/* .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>
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
<section class="text-gray-600 body-font">
|
|
||||||
<div class="container px-5 py-24 mx-auto">
|
|
||||||
<div class="flex flex-wrap -m-4">
|
|
||||||
<div class="lg:w-1/3 lg:mb-0 mb-6 p-4">
|
|
||||||
<div class="h-full text-center">
|
|
||||||
<img alt="testimonial" class="w-20 h-20 mb-8 object-cover object-center rounded-full inline-block border-2 border-gray-200 bg-gray-100" src="https://dummyimage.com/302x302">
|
|
||||||
<p class="leading-relaxed">Edison bulb retro cloud bread echo park, helvetica stumptown taiyaki taxidermy 90's cronut +1 kinfolk. Single-origin coffee ennui shaman taiyaki vape DIY tote bag drinking vinegar cronut adaptogen squid fanny pack vaporware.</p>
|
|
||||||
<span class="inline-block h-1 w-10 rounded bg-indigo-500 mt-6 mb-4"></span>
|
|
||||||
<h2 class="text-gray-900 font-medium title-font tracking-wider text-sm">HOLDEN CAULFIELD</h2>
|
|
||||||
<p class="text-gray-500">Senior Product Designer</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-1/3 lg:mb-0 mb-6 p-4">
|
|
||||||
<div class="h-full text-center">
|
|
||||||
<img alt="testimonial" class="w-20 h-20 mb-8 object-cover object-center rounded-full inline-block border-2 border-gray-200 bg-gray-100" src="https://dummyimage.com/300x300">
|
|
||||||
<p class="leading-relaxed">Edison bulb retro cloud bread echo park, helvetica stumptown taiyaki taxidermy 90's cronut +1 kinfolk. Single-origin coffee ennui shaman taiyaki vape DIY tote bag drinking vinegar cronut adaptogen squid fanny pack vaporware.</p>
|
|
||||||
<span class="inline-block h-1 w-10 rounded bg-indigo-500 mt-6 mb-4"></span>
|
|
||||||
<h2 class="text-gray-900 font-medium title-font tracking-wider text-sm">ALPER KAMU</h2>
|
|
||||||
<p class="text-gray-500">UI Develeoper</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="lg:w-1/3 lg:mb-0 p-4">
|
|
||||||
<div class="h-full text-center">
|
|
||||||
<img alt="testimonial" class="w-20 h-20 mb-8 object-cover object-center rounded-full inline-block border-2 border-gray-200 bg-gray-100" src="https://dummyimage.com/305x305">
|
|
||||||
<p class="leading-relaxed">Edison bulb retro cloud bread echo park, helvetica stumptown taiyaki taxidermy 90's cronut +1 kinfolk. Single-origin coffee ennui shaman taiyaki vape DIY tote bag drinking vinegar cronut adaptogen squid fanny pack vaporware.</p>
|
|
||||||
<span class="inline-block h-1 w-10 rounded bg-indigo-500 mt-6 mb-4"></span>
|
|
||||||
<h2 class="text-gray-900 font-medium title-font tracking-wider text-sm">HENRY LETHAM</h2>
|
|
||||||
<p class="text-gray-500">CTO</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
|
@ -2,21 +2,21 @@ import { dev } from '$app/environment';
|
||||||
|
|
||||||
export const title = "KkosmetickySalon";
|
export const title = "KkosmetickySalon";
|
||||||
export const description = 'Salon kosmetiky, krásy, půvabu, kde se můžete nechat hýčkat a zkrášlovat.';
|
export const description = 'Salon kosmetiky, krásy, půvabu, kde se můžete nechat hýčkat a zkrášlovat.';
|
||||||
export const url = dev ? 'http://localhost:5174' : 'https://kkosmetickysalon.cz';
|
export const owner = 'Klára Morinová';
|
||||||
export const author = 'Klára Morinová';
|
export const address = 'Oldřichov 38, Písek 39701'
|
||||||
|
|
||||||
export const email = 'klara@kkosmetickysalon.cz';
|
export const url = dev ? 'http://localhost:5174' : 'https://kkosmetickysalon.cz';
|
||||||
export const facebook = 'https://www.facebook.com/jack.morin.712';
|
|
||||||
export const instagram = 'https://www.instagram.com/kkosmetickysalon/';
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export const socialLinks = [
|
export const socialLinks = [
|
||||||
{ title: 'Instagram', href: 'https://www.instagram.com/kkosmetickysalon/', icon: 'fa-brands fa-linkedin'},
|
{ 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'},
|
{ title: 'Email', href: 'klara@kkosmetickysalon.cz', icon: 'fa-regular fa-envelope'},
|
||||||
|
{ title: 'Facebook', href: 'https://www.facebook.com/jack.morin.712', icon: 'fa-brands fa-facebook'},
|
||||||
|
{ title: 'Phone', href: 'tel:+420792304497', icon: 'fa-regular fa-phone' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const FBNumID = 420694206942069
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
export const NavRoutes = [
|
export const NavRoutes = [
|
||||||
{ title: 'Home', href: '/' },
|
{ title: 'Home', href: '/' },
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import type { Writable } from 'svelte/store';
|
|
||||||
import type { InstagramPost } from '../types/instagram';
|
|
||||||
|
|
||||||
const instagramFeed: Writable<InstagramPost[]> = writable([]);
|
|
||||||
|
|
||||||
|
|
||||||
export default instagramFeed;
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type ImageLinkArgs = {
|
||||||
|
urlOrPublicId: string;
|
||||||
|
h: number;
|
||||||
|
w: number;
|
||||||
|
max?: boolean;
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
export type InstagramPost = {
|
export interface InstagramPost {
|
||||||
id: string;
|
id: string;
|
||||||
media_type: string;
|
media_type: string;
|
||||||
media_url: string;
|
media_url: string;
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
export interface MarkdownHeading {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
level: number;
|
||||||
|
children: MarkdownHeading[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownMetadata {
|
||||||
|
headings: MarkdownHeading[];
|
||||||
|
frontmatter: MarkdownFrontmatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for markdown frontmatter
|
||||||
|
export interface MarkdownFrontmatter {
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
tags: Tag[];
|
||||||
|
published?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tag = 'PMU' | 'permanentní makeup' | 'vizáž' | 'depilace' | 'vakuslim' | 'ošetření' | 'makeup' | 'pleť' | 'péče' | 'beauty' | 'salon' | 'salón' | 'kosmetika' | '';
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type OGType = "Article" | "Website";
|
||||||
|
export type OGImageType = "image/png" | "image/jpg" | "image/webp" | "image/gif" // fill this out and correct it please
|
||||||
|
export type OGImageHeight = 512
|
||||||
|
export type OGImageWidth = 1024
|
||||||
|
// make the logic so that we have either a image width and heigh a 2:1 or 16:9 ratio , ie choose one or the other
|
||||||
|
// should I declare other properties when they will be defined outside of this type in the component? I guess url will be gotten conditionally
|
||||||
|
export interface OpenGraphMetadata {
|
||||||
|
type: OGType;
|
||||||
|
image: string;
|
||||||
|
imageType: OGImageType;
|
||||||
|
imageWidth: OGImageWidth;
|
||||||
|
imageHeight: OGImageHeight;
|
||||||
|
canonical: string;
|
||||||
|
};
|
|
@ -1,8 +0,0 @@
|
||||||
export type Product = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
description: string;
|
|
||||||
image: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
|
@ -1,10 +1,24 @@
|
||||||
|
import type { MarkdownMetadata } from '$lib/types/mdMetadata';
|
||||||
|
import type { OpenGraphMetadata } from '$lib/types/ogMetadata';
|
||||||
|
|
||||||
|
// Base service item type
|
||||||
export type Service = {
|
export type Service = {
|
||||||
category: string;
|
title: string;
|
||||||
items: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
description: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
image: string;
|
||||||
price: number | string;
|
price: number | string;
|
||||||
duration: number | string;
|
duration: number | string;
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
image: string;
|
||||||
|
services: Service[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended service item type with OpenGraph metadata
|
||||||
|
export type ExtendedService = Service & OpenGraphMetadata & MarkdownMetadata;
|
||||||
|
export type ExtendedCategory = Category & OpenGraphMetadata & MarkdownMetadata;
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Formats a date string into a human-readable format.
|
||||||
|
* @param date - The date string to format.
|
||||||
|
* @returns A string representing the formatted date, or an empty string if the input is invalid.
|
||||||
|
*/
|
||||||
|
export const formatDate = (date: string) => {
|
||||||
|
try {
|
||||||
|
const d = new Date(date);
|
||||||
|
return `${d.toLocaleString('default', {
|
||||||
|
month: 'long'
|
||||||
|
})} ${d.getDate()}, ${d.getFullYear()}`;
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a string and returns a beautified version of it.
|
||||||
|
* @param str The input string to be beautified.
|
||||||
|
* @returns The beautified string.
|
||||||
|
*/
|
||||||
|
export const stringToBeautifiedFragment = (str = '') =>
|
||||||
|
(str || '').toLocaleLowerCase().replace(/\s/g, '-').replace(/\?/g, '').replace(/,/g, '');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the trailing slash from a given string.
|
||||||
|
* @param site - The string to remove the trailing slash from.
|
||||||
|
* @returns The string without the trailing slash.
|
||||||
|
*/
|
||||||
|
export const removeTrailingSlash = (site: string) => {
|
||||||
|
return site.replace(/\/$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======= JSON PARSER ========
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const readJsonFile = async (filePath: string) => {
|
||||||
|
const jsonData = await fs.readFileSync(path.join(process.cwd(), 'src', 'content', filePath), 'utf-8');
|
||||||
|
return JSON.parse(jsonData);
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// import type { Category } from '$lib/types/service';
|
||||||
|
// import fs from 'fs';
|
||||||
|
// import path from 'path';
|
||||||
|
|
||||||
|
// export const getCategories = async (): Promise<Category[]> => {
|
||||||
|
// const contentDir = await getContentDir();
|
||||||
|
// const categories: Category[] = [];
|
||||||
|
|
||||||
|
// for (const categoryDir of contentDir.dirs) {
|
||||||
|
// const categoryData = await import(`../content/${categoryDir.name}/category.json`);
|
||||||
|
// const services = await Promise.all(
|
||||||
|
// categoryDir.files
|
||||||
|
// .filter((file) => file.name.endsWith('.md'))
|
||||||
|
// .map(async (file) => {
|
||||||
|
// const { metadata, content } = await getMetadata(file.content);
|
||||||
|
// return {
|
||||||
|
// ...metadata,
|
||||||
|
// content,
|
||||||
|
// };
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
|
// categories.push({
|
||||||
|
// ...categoryData.default,
|
||||||
|
// services,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return categories;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // -----------------
|
||||||
|
// const contentPath: string = path.join(process.cwd(), 'src', 'content');
|
||||||
|
|
||||||
|
// const getCategories = async ( contentPath: string ): Promise<Category[]> => {
|
||||||
|
// try {
|
||||||
|
// // read directories - entries - in contentPath
|
||||||
|
// const entries = await fs.promises.readdir( contentPath );
|
||||||
|
|
||||||
|
// const categoryDirs = await Promise.all(
|
||||||
|
// entries.map(async (entry) => {
|
||||||
|
// // /src/content/example-service
|
||||||
|
// const entryPath = path.join(contentPath, entry);
|
||||||
|
// // file statistict
|
||||||
|
// const stats = fs.promises.stat(entryPath);
|
||||||
|
|
||||||
|
// // Ensure stats is an object before accessing properties
|
||||||
|
// if (typeof stats === 'object' && stats !== null && 'isDirectory' in stats) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// return
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
import type { Category, Service } from '$lib/types/service';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export const getCategories = async (): Promise<Category[]> => {
|
||||||
|
const contentDir = path.join(process.cwd(), 'src', 'content');
|
||||||
|
const categoryDirs = fs.readdirSync(contentDir, { withFileTypes: true })
|
||||||
|
.filter(dirent => dirent.isDirectory())
|
||||||
|
.map(dirent => dirent.name);
|
||||||
|
|
||||||
|
const categories: Category[] = await Promise.all(
|
||||||
|
categoryDirs.map(async categoryId => {
|
||||||
|
const categoryJsonPath = path.join(contentDir, categoryId, `${categoryId}.json`);
|
||||||
|
const categoryData = JSON.parse(fs.readFileSync(categoryJsonPath, 'utf-8'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...categoryData,
|
||||||
|
services: categoryData.services.map((service: Service) => ({
|
||||||
|
...service,
|
||||||
|
id: `${categoryId}/${service.id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
// ======= MARKDOWN PARSER ========
|
||||||
|
|
||||||
|
// https://github.com/jonschlinkert/gray-matter
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
// https://github.com/markedjs/marked;- unused
|
||||||
|
// import marked from 'marked';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export const parseMarkdownFile = async (filePath: string) => {
|
||||||
|
const markdownData = fs.readFileSync(path.join(process.cwd(), 'src', 'content', filePath), 'utf-8');
|
||||||
|
const { data, content } = matter(markdownData);
|
||||||
|
return { frontmatter: data, content };
|
||||||
|
}
|
||||||
|
// export function parseMarkdown<T>(filePath: string): { frontmatter: T; content: string } {
|
||||||
|
// const data = matter.read(filePath).data;
|
||||||
|
// return {
|
||||||
|
// frontmatter: data as T,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
console.log(parseMarkdownFile('../../content/permanentni-make-up/pmu/pmu.md'))
|
|
@ -0,0 +1,49 @@
|
||||||
|
// https://cloudinary.com/blog/guest_post/setup-a-developer-blog-with-social-images-using-sveltekit
|
||||||
|
|
||||||
|
import cloudinary from 'cloudinary';
|
||||||
|
|
||||||
|
cloudinary.v2.config({
|
||||||
|
cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
async function getOgImage({ title, subTitle }) {
|
||||||
|
const url = cloudinary.v2.url('example_og_image.jpg', {
|
||||||
|
transformation: [
|
||||||
|
{ width: 1200, height: 627, crop: 'fill', quality: 'auto', format: 'auto' },
|
||||||
|
{
|
||||||
|
crop: 'fit',
|
||||||
|
width: 700,
|
||||||
|
x: 480,
|
||||||
|
y: 254,
|
||||||
|
gravity: 'south_west',
|
||||||
|
color: 'white',
|
||||||
|
effect: 'shadow:40',
|
||||||
|
overlay: {
|
||||||
|
font_family: 'roboto',
|
||||||
|
font_size: 54,
|
||||||
|
font_weight: 'bold',
|
||||||
|
text: encodeURIComponent(title)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
crop: 'fit',
|
||||||
|
width: 700,
|
||||||
|
x: 480,
|
||||||
|
y: 154,
|
||||||
|
gravity: 'south_west',
|
||||||
|
color: 'white',
|
||||||
|
overlay: {
|
||||||
|
font_family: 'roboto',
|
||||||
|
font_size: 34,
|
||||||
|
font_weight: 'bold',
|
||||||
|
text: encodeURIComponent(subTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getOgImage;
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { readable } from 'svelte/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current timezone is between 0 and +3 hours UTC.
|
||||||
|
* @returns {boolean} True if the timezone is between 0 and +3 hours UTC, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isEurope = () => {
|
||||||
|
const offset = new Date().getTimezoneOffset();
|
||||||
|
return offset <= 0 && offset >= -180; // Returns true if the timezone is between 0 and +3 hours UTC, false otherwise
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the 'overflow-y-hidden' class on the 'html' element of the document.
|
||||||
|
* @param bool A boolean value indicating whether to show or hide the overflow-y scrollbar.
|
||||||
|
*/
|
||||||
|
export const showHideOverflowY = (bool: boolean) => {
|
||||||
|
const html = document.querySelector('html');
|
||||||
|
if (html) {
|
||||||
|
if (bool) {
|
||||||
|
html.classList.add('overflow-y-hidden');
|
||||||
|
} else {
|
||||||
|
html.classList.remove('overflow-y-hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given URL is an external link.
|
||||||
|
* @param href - The URL to check.
|
||||||
|
* @returns True if the URL is an external link, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isAnExternalLink = (href: string) => href.startsWith('http');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user agent is running on a Mac or iPad.
|
||||||
|
* @returns {boolean} Returns true if the user agent is running on a Mac or iPad, false otherwise.
|
||||||
|
*/
|
||||||
|
export const isMac = () =>
|
||||||
|
navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('iPad');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a readable store that tracks whether the media query string matches the current viewport.
|
||||||
|
* @param mediaQueryString - The media query string to match against the viewport.
|
||||||
|
* @returns A readable store that tracks whether the media query string matches the current viewport.
|
||||||
|
*/
|
||||||
|
export const useMediaQuery = (mediaQueryString: string) => {
|
||||||
|
const matches = readable<boolean | undefined>(undefined, (set) => {
|
||||||
|
if (typeof globalThis['window'] === 'undefined') return;
|
||||||
|
|
||||||
|
const match = window.matchMedia(mediaQueryString);
|
||||||
|
set(match.matches);
|
||||||
|
const element = (event: MediaQueryListEvent) => set(event.matches);
|
||||||
|
match.addEventListener('change', element);
|
||||||
|
return () => {
|
||||||
|
match.removeEventListener('change', element);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// imageService.ts
|
||||||
|
import cloudinary from 'cloudinary';
|
||||||
|
import imagesData from '$content/images.json';
|
||||||
|
|
||||||
|
cloudinary.v2.config({
|
||||||
|
cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getCloudinaryImageUrl = (publicId: string, options: cloudinary.UploadApiOptions = {}) => {
|
||||||
|
const imageData = imagesData[publicId];
|
||||||
|
if (!imageData) {
|
||||||
|
throw new Error(`Image with public ID ${publicId} not found in images.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformationOptions = {
|
||||||
|
...options,
|
||||||
|
...imageData.transformations,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cloudinary.v2.url(publicId, transformationOptions);
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { readJsonFile, parseMarkdownFile } from '../utils/formatParse';
|
||||||
|
|
||||||
|
export function listservices({params}) {
|
||||||
|
const servicesJson = readJsonFile('./path/to/services.json');
|
||||||
|
const services = servicesJson.map((service: Service) => {
|
||||||
|
const markdownData = parseMarkdownFile(`./path/to/blog/${service.slug}.md`);
|
||||||
|
return { ...service, ...markdownData };
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
|
const visitor = (node) => {
|
||||||
|
node.data = node.data || {};
|
||||||
|
node.data.hProperties = node.data.hProperties || {};
|
||||||
|
if (node.type === 'link') {
|
||||||
|
if (
|
||||||
|
node.children &&
|
||||||
|
node.children.length &&
|
||||||
|
node.children.length === 1
|
||||||
|
) {
|
||||||
|
if (node.children[0].type === 'image') {
|
||||||
|
node.data.hProperties.class = 'after:hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => async (tree) => {
|
||||||
|
visit(tree, visitor);
|
||||||
|
return tree;
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
|
const imagesRelativeUrlPattern = '/images/';
|
||||||
|
|
||||||
|
const visitor = (node) => {
|
||||||
|
if (node.type === 'image' && node.url.indexOf(imagesRelativeUrlPattern) > 0) {
|
||||||
|
node.url = node.url.substring(node.url.indexOf(imagesRelativeUrlPattern) + ''.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => async (tree, vFile) => {
|
||||||
|
if (
|
||||||
|
vFile.filename.indexOf('src/routes/blog/') > 0 ||
|
||||||
|
vFile.filename.indexOf('src/routes/projects/') > 0
|
||||||
|
) {
|
||||||
|
visit(tree, visitor);
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Scrolls the page to the nearest element matching the given selector.
|
||||||
|
* @param selector - The CSS selector of the element to scroll to.
|
||||||
|
*/
|
||||||
|
export const scrollIntoView = (selector: string) => {
|
||||||
|
const scrollToElement = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (!scrollToElement) return;
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
|
||||||
|
scrollToElement.scrollIntoView({
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'start',
|
||||||
|
behavior: mediaQuery.matches ? 'auto' : 'smooth'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the first element that matches the given selector within the provided element.
|
||||||
|
* @param element The element to search within.
|
||||||
|
* @param selector The selector to match against.
|
||||||
|
*/
|
||||||
|
export const scrollToElement = async (element: HTMLElement, selector: string) => {
|
||||||
|
const firstElement: HTMLElement | null = element.querySelector(selector);
|
||||||
|
if (!firstElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
firstElement.scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
// uploadImages.ts
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import cloudinary from 'cloudinary';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
|
cloudinary.v2.config({
|
||||||
|
cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
|
||||||
|
api_key: import.meta.env.VITE_CLOUDINARY_API_KEY,
|
||||||
|
api_secret: import.meta.env.VITE_CLOUDINARY_API_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentDir = 'src/content';
|
||||||
|
const imagesJsonPath = 'src/content/images.json';
|
||||||
|
|
||||||
|
async function uploadImages() {
|
||||||
|
const imagesData = {};
|
||||||
|
|
||||||
|
// Read all Markdown files in the content directory
|
||||||
|
const mdFiles = fs.readdirSync(contentDir).filter((file) => file.endsWith('.md'));
|
||||||
|
|
||||||
|
for (const mdFile of mdFiles) {
|
||||||
|
const mdFilePath = path.join(contentDir, mdFile);
|
||||||
|
const mdContent = fs.readFileSync(mdFilePath, 'utf-8');
|
||||||
|
const { data, content } = matter(mdContent);
|
||||||
|
|
||||||
|
// Extract relative image paths from the Markdown content
|
||||||
|
const imagePaths = content.match(/\!\[.*?\]\((.*?)\)/g) || [];
|
||||||
|
|
||||||
|
for (const imagePath of imagePaths) {
|
||||||
|
const relativeImagePath = imagePath.match(/\((.*?)\)/)[1];
|
||||||
|
const imageFilePath = path.join(contentDir, relativeImagePath);
|
||||||
|
|
||||||
|
// Upload the image to Cloudinary
|
||||||
|
const uploadResult = await cloudinary.v2.uploader.upload(imageFilePath);
|
||||||
|
|
||||||
|
// Add the image metadata to the imagesData object
|
||||||
|
imagesData[uploadResult.public_id] = {
|
||||||
|
publicId: uploadResult.public_id,
|
||||||
|
// Add any desired transformation options here
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the imagesData to the images.json file
|
||||||
|
fs.writeFileSync(imagesJsonPath, JSON.stringify(imagesData, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImages();
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import HeroSection from "$lib/components/hero/HeroSection.svelte";
|
import Map from '$lib/components/Map.svelte';
|
||||||
|
import HeroSection from '$lib/components/hero/HeroSection.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
@ -24,12 +25,7 @@
|
||||||
</figure>
|
</figure>
|
||||||
<!-- / -->
|
<!-- / -->
|
||||||
<div class="flex justify-center space-x-2">
|
<div class="flex justify-center space-x-2">
|
||||||
<a
|
<a class="btn variant-filled" href="https://skeleton.dev/" target="_blank" rel="noreferrer">
|
||||||
class="btn variant-filled"
|
|
||||||
href="https://skeleton.dev/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
O mně
|
O mně
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,28 +34,8 @@
|
||||||
<p><code class="code">/src/routes/+layout.svelte</code></p>
|
<p><code class="code">/src/routes/+layout.svelte</code></p>
|
||||||
<p><code class="code">/src/routes/+page.svelte</code></p>
|
<p><code class="code">/src/routes/+page.svelte</code></p>
|
||||||
</div>
|
</div>
|
||||||
<section class="relative">
|
<Map />
|
||||||
<div class="container px-5 py-24 mx-auto flex sm:flex-nowrap flex-wrap">
|
|
||||||
<div class="lg:w-2/3 md:w-1/2 bg-gray-300 rounded-lg overflow-hidden sm:mr-10 p-10 flex items-end justify-start relative">
|
|
||||||
<iframe class="absolute inset-0" width="100%" height="100%" src="https://www.openstreetmap.org/export/embed.html?bbox=14.10176753997803%2C49.31640483169278%2C14.10820484161377%2C49.319359677506206&layer=mapnik&marker=49.31788227675921%2C14.104986190795898" title="map" marginheight="0" marginwidth="0" scrolling="no" style="filter: grayscale(1) contrast(1.2) opacity(0.4);" frameborder="0"></iframe>
|
|
||||||
<br/><small><a href="https://www.openstreetmap.org/?mlat=49.31788&mlon=14.10499#map=18/49.31788/14.10499">View Larger Map</a></small>
|
|
||||||
<div class="bg-white relative flex flex-wrap py-6 rounded shadow-md">
|
|
||||||
<div class="lg:w-1/2 px-6">
|
|
||||||
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs">ADDRESS</h2>
|
|
||||||
<p class="mt-1">Photo booth tattooed prism, portland taiyaki hoodie neutra typewriter</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:w-1/2 px-6 mt-4 lg:mt-0">
|
|
||||||
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs">EMAIL</h2>
|
|
||||||
<a class="text-indigo-500 leading-relaxed">example@email.com</a>
|
|
||||||
<h2 class="title-font font-semibold text-gray-900 tracking-widest text-xs mt-4">PHONE</h2>
|
|
||||||
<p class="leading-relaxed">123-456-7890</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
@ -72,8 +48,7 @@
|
||||||
}
|
}
|
||||||
.img-bg {
|
.img-bg {
|
||||||
@apply absolute z-[-1] rounded-full blur-[50px] transition-all;
|
@apply absolute z-[-1] rounded-full blur-[50px] transition-all;
|
||||||
animation: pulse 5s cubic-bezier(0, 0, 0, 0.5) infinite,
|
animation: pulse 5s cubic-bezier(0, 0, 0, 0.5) infinite, glow 5s linear infinite;
|
||||||
glow 5s linear infinite;
|
|
||||||
}
|
}
|
||||||
@keyframes glow {
|
@keyframes glow {
|
||||||
0% {
|
0% {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export const csr = false;
|
|
@ -23,30 +23,39 @@
|
||||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Cookies from '$lib/components/Cookies.svelte';
|
|
||||||
import MainHeader from '$lib/components/MainHeader.svelte';
|
import MainHeader from '$lib/components/MainHeader.svelte';
|
||||||
import MainFooter from '$lib/components/MainFooter.svelte';
|
import MainFooter from '$lib/components/MainFooter.svelte';
|
||||||
|
|
||||||
// SEO Meta tags
|
// SEO Meta tags
|
||||||
const metaDefaults = {
|
import SEO from '$lib/components/SEO.svelte'
|
||||||
title: 'BeautySalon',
|
|
||||||
description: 'BeautySalon Popis.',
|
$: seoData = $page.data.seoData || {
|
||||||
image: 'https://user-images.githubusercontent.com/1509726/212382766-f29b9c9a-82e3-44c2-b911-b17a9197e5b9.jpg'
|
title: 'KKosmetickySalon - Klára Morinová',
|
||||||
};
|
description: 'Přijeďte si ke mně pro odbornou péči o Vaši pleť. Jsem certifikovaná kosmetička a makeup artistka, provádím Permanentní Make-up, vizážistiku, ošetřovací a zeštihlovací procedury, a mnoho dalšího... Vše s ohledem na vaše zdraví. Zarezervujte si péči o svůj vzhled přes moje stránky nyní!',
|
||||||
const meta = {
|
keywords: 'kosmetika, vizáž, pleť, Písek, kosmetička, permanentní makeup, ',
|
||||||
title: metaDefaults.title,
|
image: '/logo-text.png',
|
||||||
description: metaDefaults.description,
|
type: 'website',
|
||||||
image: metaDefaults.image,
|
|
||||||
// Article
|
|
||||||
article: { publishTime: '', modifiedTime: '', author: '' },
|
|
||||||
// Twitter
|
|
||||||
twitter: {
|
|
||||||
title: metaDefaults.title,
|
|
||||||
description: metaDefaults.description,
|
|
||||||
image: metaDefaults.image
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const metaDefaults = {
|
||||||
|
// title: 'BeautySalon',
|
||||||
|
// description: 'BeautySalon Popis.',
|
||||||
|
// image: 'https://user-images.githubusercontent.com/1509726/212382766-f29b9c9a-82e3-44c2-b911-b17a9197e5b9.jpg'
|
||||||
|
// };
|
||||||
|
// const meta = {
|
||||||
|
// title: metaDefaults.title,
|
||||||
|
// description: metaDefaults.description,
|
||||||
|
// image: metaDefaults.image,
|
||||||
|
// // Article
|
||||||
|
// article: { publishTime: '', modifiedTime: '', author: '' },
|
||||||
|
// // Twitter
|
||||||
|
// twitter: {
|
||||||
|
// title: metaDefaults.title,
|
||||||
|
// description: metaDefaults.description,
|
||||||
|
// image: metaDefaults.image
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
// Scroll to anchor
|
// Scroll to anchor
|
||||||
$: if ($page.url.pathname) {
|
$: if ($page.url.pathname) {
|
||||||
|
@ -64,15 +73,15 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- SEO -->
|
<!-- SEO -->
|
||||||
<svelte:head>
|
<!-- <svelte:head>
|
||||||
<title>{meta.title}</title>
|
<title>{meta.title}</title>
|
||||||
<!-- Meta Tags -->
|
Meta Tags
|
||||||
<meta name="title" content={meta.title} />
|
<meta name="title" content={meta.title} />
|
||||||
<meta name="description" content={meta.description} />
|
<meta name="description" content={meta.description} />
|
||||||
<meta name="keywords" content="krása, kosmetika, permanentní makeup, revitalizace pleti, oprava pleti, salón, kosmetička, kosmetický salón, Písek, obočí, vytrhání, natočení řas, řasy" />
|
<meta name="keywords" content="krása, kosmetika, permanentní makeup, revitalizace pleti, oprava pleti, salón, kosmetička, kosmetický salón, Písek, obočí, vytrhání, natočení řas, řasy" />
|
||||||
<meta name="theme-color" content="#242c46" />
|
<meta name="theme-color" content="#242c46" />
|
||||||
<meta name="author" content="Klára Morinová" />
|
<meta name="author" content="Klára Morinová" />
|
||||||
<!-- Open Graph - https://ogp.me/ -->
|
Open Graph - https://ogp.me/
|
||||||
<meta property="og:site_name" content="BeautySalon" />
|
<meta property="og:site_name" content="BeautySalon" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://www.beautysalon.cz{$page.url.pathname}" />
|
<meta property="og:url" content="https://www.beautysalon.cz{$page.url.pathname}" />
|
||||||
|
@ -85,12 +94,12 @@
|
||||||
<meta property="og:image:width" content="1707" />
|
<meta property="og:image:width" content="1707" />
|
||||||
<meta property="og:image:height" content="1233" />
|
<meta property="og:image:height" content="1233" />
|
||||||
|
|
||||||
<!-- Open Graph: Twitter -->
|
Open Graph: Twitter
|
||||||
<meta name="twitter:title" content={meta.twitter.title} />
|
<meta name="twitter:title" content={meta.twitter.title} />
|
||||||
<meta name="twitter:description" content={meta.twitter.description} />
|
<meta name="twitter:description" content={meta.twitter.description} />
|
||||||
<meta name="twitter:image" content={meta.twitter.image} />
|
<meta name="twitter:image" content={meta.twitter.image} />
|
||||||
</svelte:head>
|
</svelte:head> -->
|
||||||
|
<SEO {seoData}/>
|
||||||
<!-- <Analytics /> -->
|
<!-- <Analytics /> -->
|
||||||
<!-- App Shell -->
|
<!-- App Shell -->
|
||||||
<Drawer />
|
<Drawer />
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PUBLIC_MAPBOX_ACCESS_TOKEN } from '$env/static/public';
|
import Map from "$lib/components/Map.svelte";
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Kde mě najdete? Kde mě můžete kontaktovat?</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Map></Map>
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<!-- Úvod -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<!-- Foto -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<!-- Certifikace -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<!-- Foto -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<!-- -->
|
||||||
|
</section>
|
|
@ -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>
|
|
|
@ -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");
|
|
||||||
};
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { removeTrailingSlash } from '$lib/utils/formatParse'
|
||||||
|
import { listPosts } from '$content/blog';
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const sitemap = (pages: string[]) => `<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<urlset
|
||||||
|
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
|
||||||
|
xmlns:xhtml="https://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
|
||||||
|
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
|
||||||
|
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
|
||||||
|
>
|
||||||
|
${pages.map((page) => `<url><loc>${removeTrailingSlash(page)}</loc></url>`).join('')}
|
||||||
|
</urlset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const staticPages = Object.keys(
|
||||||
|
// For other static pages. Except content pages - changelogs, guides, blog posts, guides etc.
|
||||||
|
import.meta.glob('/src/routes/**/!(_)*.{svelte,md,svx}')
|
||||||
|
)
|
||||||
|
.filter((page) => {
|
||||||
|
const filters = [
|
||||||
|
'/src/routes/index.svelte',
|
||||||
|
'404',
|
||||||
|
'slug]',
|
||||||
|
'title]',
|
||||||
|
'+error',
|
||||||
|
'+layout'
|
||||||
|
];
|
||||||
|
return !filters.find((filter) => page.includes(filter));
|
||||||
|
})
|
||||||
|
.map((page) => {
|
||||||
|
return page
|
||||||
|
.replace('/src/routes', 'https://mattmor.in')
|
||||||
|
.replace('/index.md', '/')
|
||||||
|
.replace('.md', '/')
|
||||||
|
.replace('/index.svelte', '/')
|
||||||
|
.replace('.svelte', '/')
|
||||||
|
.replace('/+page', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
const Posts = listPosts().map((post) => `https://www.mattmor.in/blog/${post.slug}`);
|
||||||
|
const renderedSitemap = sitemap([...staticPages, ...Posts]);
|
||||||
|
|
||||||
|
return new Response(renderedSitemap, {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'max-age=0, s-maxage=3600',
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { PageServerLoad } from './$types';
|
||||||
|
import { readJsonFile } from '$lib/utils/formatParse';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
const categoriesDir = path.join(process.cwd(), 'src', 'content');
|
||||||
|
const categoryDirs = fs.readdirSync(categoriesDir).filter(dir => fs.lstatSync(path.join(categoriesDir, dir)).isDirectory());
|
||||||
|
|
||||||
|
const services = categoryDirs.map(categoryDir => {
|
||||||
|
const category = readJsonFile(`${categoryDirs}/${categoryDir}.json`);
|
||||||
|
return { category: categoryDir, items: category };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { props: { services } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const load: = async () => {
|
||||||
|
// return {
|
||||||
|
// posts: listPosts()
|
||||||
|
// };
|
||||||
|
// };
|
|
@ -1,73 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Spinner from '$lib/components/Spinner.svelte';
|
||||||
import ServicesLayout from '$lib/components/services/ServicesLayout.svelte';
|
import ServicesLayout from '$lib/components/services/ServicesLayout.svelte';
|
||||||
import type { Service } from '$lib/types/service';
|
import type { Service } from '$lib/types/service';
|
||||||
export const id = '';
|
export let services: Service[] = []
|
||||||
|
|
||||||
// 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ě' }
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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Í', 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 },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if props.services.length === 0}
|
||||||
|
<Spinner/>
|
||||||
|
<p>Loading services...</p>
|
||||||
|
{:else}
|
||||||
<div class="flex flex-col items-center py-8">
|
<div class="flex flex-col items-center py-8">
|
||||||
<h1 class="h1 m-4">Ceník služeb</h1>
|
<h1 class="h1 m-4">Ceník služeb</h1>
|
||||||
<p class="text-lg text-center font-semibold m-4 pb-8">Koukněte se jak vás mohu zkrášlit a rezervujte si termín</p>
|
<p class="text-lg text-center font-semibold m-4 pb-8">Koukněte se jak vás mohu zkrášlit a rezervujte si termín</p>
|
||||||
<ServicesLayout {services} />
|
<ServicesLayout {services} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// import type { PageServerLoad } from './$types';
|
||||||
|
// import path from 'path';
|
||||||
|
// import fs from 'fs';
|
||||||
|
// import { error } from '@sveltejs/kit';
|
||||||
|
// import { fetchServiceData } from '$lib/utils/fetchServiceData';
|
||||||
|
|
||||||
|
// export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
// const { categoryId, serviceId } = params;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const serviceData = await fetchServiceData(categoryId, serviceId);
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// service: serviceData
|
||||||
|
// };
|
||||||
|
// } catch (err) {
|
||||||
|
// throw error(404, `Service ${serviceId} not found`);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// src/routes/[categoryId]/[serviceId]/+page.server.ts
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { getCategories } from '$lib/utils/getCategories';
|
||||||
|
|
||||||
|
export const load = async ({ params }) => {
|
||||||
|
const { categoryId, serviceId } = params;
|
||||||
|
const categories = await getCategories();
|
||||||
|
const category = categories.find(cat => cat.id === categoryId);
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
throw error(404, 'Category not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = category.services.find(serv => serv.id === `${categoryId}/${serviceId}`);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw error(404, 'Service not found');
|
||||||
|
}
|
||||||
|
// mdsvex
|
||||||
|
const {default: content, metadata: frontmatter} = await import(`$content/${categoryId}/${serviceId}.md`)
|
||||||
|
|
||||||
|
const seoData = {
|
||||||
|
title: service.title, // Assuming title exists in service data
|
||||||
|
description: service.description,
|
||||||
|
id: service.id,
|
||||||
|
image: service.image,
|
||||||
|
price: service.price,
|
||||||
|
duration: service.duration,
|
||||||
|
// ... (populate other optional fields)
|
||||||
|
frontmatter: frontmatter, // Assuming metadata contains date and tags
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
category,
|
||||||
|
service: {
|
||||||
|
...service,
|
||||||
|
content,
|
||||||
|
frontmatter
|
||||||
|
},
|
||||||
|
seoData
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!-- src/routes/[categoryId]/[serviceId]/+page.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as conf from '$lib/config'
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import { SEO } from '$lib/components/SEO.svelte';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SEO seoData={data}/>
|
||||||
|
|
||||||
|
<!-- Your service content goes here -->
|
||||||
|
|
|
@ -1,33 +1,69 @@
|
||||||
import adapter from '@sveltejs/adapter-node';
|
// svelte adapter
|
||||||
import preprocess from 'svelte-preprocess';
|
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
import adapterNode from '@sveltejs/adapter-node'
|
||||||
const config = {
|
import adapterVercel from '@sveltejs/adapter-vercel'
|
||||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
import adapterNetlify from '@sveltejs/adapter-netlify'
|
||||||
// for more information about preprocessors
|
import adapterCloudflare from '@sveltejs/adapter-cloudflare'
|
||||||
preprocess: [
|
import adapterStatic from '@sveltejs/adapter-static'
|
||||||
preprocess({
|
// svelte preprocessor
|
||||||
postcss: true
|
import { mdsvex } from 'mdsvex'
|
||||||
}),
|
import mdsvexConfig from './mdsvex.config.js'
|
||||||
],
|
// import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
import preprocess from 'svelte-preprocess'
|
||||||
|
|
||||||
|
function getAdapter() {
|
||||||
|
if (Object.keys(process.env).some(key => key.includes('VERCEL'))) {
|
||||||
|
return adapterVercel()
|
||||||
|
} else if (Object.keys(process.env).some(key => key.includes('NETLIFY'))) {
|
||||||
|
return adapterNetlify()
|
||||||
|
} else if (Object.keys(process.env).some(key => key.includes('CF_PAGES'))) {
|
||||||
|
return adapterCloudflare()
|
||||||
|
} else {
|
||||||
|
return process.env.ADAPTER === 'node'
|
||||||
|
? adapterNode({ out: 'build' })
|
||||||
|
: adapterStatic({
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@svletejs/kit".Config)} */
|
||||||
|
export default {
|
||||||
|
extensions: ['.svelte', ...(mdsvexConfig.extensions || [])],
|
||||||
|
preprocess: [preprocess({ postcss: true }), mdsvex(mdsvexConfig) /*, vitePreprocess()*/],
|
||||||
|
vitePlugin: {
|
||||||
|
inspector: true
|
||||||
|
},
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: getAdapter(),
|
||||||
precompress: false
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Aliases need tsconfig explicit inclusion
|
|
||||||
alias: {
|
alias: {
|
||||||
$lib: './src/lib',
|
$lib: './src/lib',
|
||||||
$root: './',
|
$root: './',
|
||||||
$src: './src',
|
$src: './src',
|
||||||
$routes: './src/routes',
|
$routes: './src/routes',
|
||||||
$content: './src/content'
|
$content: './content'
|
||||||
|
},
|
||||||
|
csrf: {
|
||||||
|
checkOrigin: process.env.NODE_ENV === 'development' ? false : true
|
||||||
|
},
|
||||||
|
prerender: {
|
||||||
|
crawl: true,
|
||||||
|
handleMissingId: 'warn',
|
||||||
|
handleHttpError: ({ status, path, referrer, referenceType, message }) => {
|
||||||
|
// Handle blog trying to prerender relative links that it can't
|
||||||
|
if (
|
||||||
|
(status == 404 && path.startsWith('/blog')) ||
|
||||||
|
path.startsWith('/projects') ||
|
||||||
|
(path.startsWith('/') && referenceType == 'linked')
|
||||||
|
) {
|
||||||
|
console.warn(`PRERENDER ignored route ${path}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
},
|
throw new Error(`${status} ${path} from ${referrer}, ~~~~~~~~~ message: ${message}~~~~~~~~~`)
|
||||||
env: {
|
}
|
||||||
publicPrefix: "PUBLIC_",
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
const ajv = new Ajv({verbose: true});
|
||||||
|
// Load and add the service schema
|
||||||
|
const serviceSchema = JSON.parse(fs.readFileSync('./src/content/schema-services.json', 'utf-8'));
|
||||||
|
ajv.addSchema(serviceSchema, 'serviceSchema');
|
||||||
|
|
||||||
const serviceSchema = JSON.parse(fs.readFileSync('./src/routes/sluzby/schema.json', 'utf-8'));
|
// Load and add the category schema
|
||||||
|
const categorySchema = JSON.parse(fs.readFileSync('./src/content/schema-categories.json', 'utf-8'));
|
||||||
|
ajv.addSchema(categorySchema, 'categorySchema');
|
||||||
|
|
||||||
|
// Compile the category schema validator
|
||||||
|
// const validate = ajv.getSchema('categorySchema');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filePath
|
||||||
|
*/
|
||||||
function validateFile(filePath) {
|
function validateFile(filePath) {
|
||||||
const ajv = new Ajv();
|
|
||||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||||
const validate = ajv.compile(serviceSchema);
|
const validate = ajv.compile(categorySchema);
|
||||||
const valid = validate(data);
|
const valid = validate(data);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new Error(`Invalid service data in ${filePath}: ${JSON.stringify(validate.errors)}`);
|
throw new Error(`Invalid service data in ${filePath}: ${JSON.stringify(validate.errors)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} directory
|
||||||
|
*/
|
||||||
function scanDirectory(directory) {
|
function scanDirectory(directory) {
|
||||||
const files = fs.readdirSync(directory);
|
const files = fs.readdirSync(directory);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
@ -34,6 +47,7 @@ export function validateServices() {
|
||||||
console.log('All services validated successfully!');
|
console.log('All services validated successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error validating services: ${error}`);
|
console.error(`Error validating services: ${error}`);
|
||||||
|
console.log(ajv.errors);
|
||||||
process.exit(1); // Exit with non-zero code to signal failure
|
process.exit(1); // Exit with non-zero code to signal failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// tests/e2e/category.test.ts
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('category page renders correctly', async ({ page }) => {
|
||||||
|
const categoryId = 'pmu';
|
||||||
|
await page.goto(`/${categoryId}`);
|
||||||
|
|
||||||
|
// Assert category title
|
||||||
|
const categoryTitle = await page.locator('h1').textContent();
|
||||||
|
expect(categoryTitle).toBeDefined();
|
||||||
|
|
||||||
|
// Assert category description
|
||||||
|
const categoryDescription = await page.locator('p').first().textContent();
|
||||||
|
expect(categoryDescription).toBeDefined();
|
||||||
|
|
||||||
|
// Assert services list
|
||||||
|
// const servicesList = page.locator('ul > li');
|
||||||
|
// await expect(servicesList).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Add more assertions as needed
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test('index page has expected h1', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.getByRole('heading', { name: 'Kosmeticky Salon' })).toBeVisible();
|
||||||
|
});
|
|
@ -0,0 +1,22 @@
|
||||||
|
// tests/e2e/service.test.ts
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('service page renders correctly', async ({ page }) => {
|
||||||
|
const categoryId = 'pmu';
|
||||||
|
const serviceId = 'pmu-linky';
|
||||||
|
await page.goto(`/${categoryId}/${serviceId}`);
|
||||||
|
|
||||||
|
// Assert service title
|
||||||
|
const serviceTitle = await page.locator('h1').textContent();
|
||||||
|
expect(serviceTitle).toBeDefined();
|
||||||
|
|
||||||
|
// Assert service description
|
||||||
|
const serviceDescription = await page.locator('p').first().textContent();
|
||||||
|
expect(serviceDescription).toBeDefined();
|
||||||
|
|
||||||
|
// Assert service content
|
||||||
|
const serviceContent = await page.locator('article').textContent();
|
||||||
|
expect(serviceContent).toBeDefined();
|
||||||
|
|
||||||
|
// Add more assertions as needed
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
import cloudinary from 'cloudinary';
|
||||||
|
import imagesData from '$content/images.json';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
|
|
||||||
|
cloudinary.v2.config({
|
||||||
|
cloud_name: env.CLOUDINARY_CLOUD_NAME,
|
||||||
|
api_key: env.CLOUDINARY_API_KEY,
|
||||||
|
api_secret: env.CLOUDINARY_API_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function validateImages() {
|
||||||
|
const invalidImages = [];
|
||||||
|
try {
|
||||||
|
for (const publicId in imagesData) {
|
||||||
|
try {
|
||||||
|
// Check if the image exists on Cloudinary
|
||||||
|
await cloudinary.v2.api.resource(publicId);
|
||||||
|
} catch (error) {
|
||||||
|
invalidImages.push(publicId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidImages.length > 0) {
|
||||||
|
console.error(`The following images are missing or invalid on Cloudinary: ${invalidImages.join(', ')}`);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('All images are valid on Cloudinary.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateImages();
|
|
@ -3,15 +3,20 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
// For JSON
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
// necessary https://kit.svelte.dev/docs/types#generated-types
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
// ==== "preserveValueImports": true,
|
||||||
// custom compiler options
|
// custom compiler options
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"target": "ES6",
|
"target": "ES2018",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
@ -26,8 +31,9 @@
|
||||||
"./src/**/*.ts",
|
"./src/**/*.ts",
|
||||||
".svelte-kit/ambient.d.ts",
|
".svelte-kit/ambient.d.ts",
|
||||||
".svelte-kit/types/**/$types.d.ts",
|
".svelte-kit/types/**/$types.d.ts",
|
||||||
"./csp-directives.ts"
|
"./csp-directives.ts",
|
||||||
],
|
"tests/**/*",
|
||||||
|
"src/content/**/*"],
|
||||||
"exclude": ["node_modules/*"]
|
"exclude": ["node_modules/*"]
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
//
|
//
|
||||||
|
|
|
@ -8,6 +8,9 @@ export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 5174,
|
port: 5174,
|
||||||
|
fs: {
|
||||||
|
allow: ['..'],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
envPrefix: "PUBLIC_",
|
envPrefix: "PUBLIC_",
|
||||||
plugins: [sentrySvelteKit({
|
plugins: [sentrySvelteKit({
|
||||||
|
@ -34,10 +37,11 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
$lib: path.resolve(__dirname, 'src', 'lib'),
|
$lib: path.resolve('.', 'src/lib'),
|
||||||
$root: path.resolve(__dirname),
|
$root: path.resolve('.'),
|
||||||
$src: path.resolve(__dirname, 'src'),
|
$src: path.resolve('.', 'src'),
|
||||||
$routes: path.resolve(__dirname, 'src', 'routes')
|
$routes: path.resolve('.', 'src/routes'),
|
||||||
|
$content: path.resolve('.', 'src/content'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue