Compare commits

..

45 Commits
rand ... master

Author SHA1 Message Date
matthieu42morin f833c9c206 vhjhhj 2024-07-03 10:10:15 +02:00
matthieu42morin 870a6f2016 fix, typo in SEO.svelte 2024-04-12 23:14:34 +02:00
matthieu42morin 347bc65360 content update 2024-04-12 23:14:09 +02:00
matthieu42morin c0ffda8394 SCHEMA - dont require description 2024-04-12 21:21:23 +02:00
Matthieu Morin aca901c2a7 SCHEMA - delete $schema , it is only a metaschema property now allowed in data, update ajv validator 2024-04-05 16:03:29 +02:00
Matthieu Morin 0115cb501c E2E tests + ajv validation of services update to match structure 2024-04-05 15:00:47 +02:00
Matthieu Morin 1a75df9851 Server Functions for getting services 2024-04-05 14:48:41 +02:00
matthieu42morin e6203baf9e Merge branch 'master' of https://git.mattmor.in/Madmin/KkosmetickySalon 2024-04-05 14:41:04 +02:00
matthieu42morin 4465d1d567 hooks env update 2024-04-05 14:38:29 +02:00
Matthieu Morin 67a2d0be1e Services layouts update + filling empty routing pages 2024-04-05 14:37:03 +02:00
Matthieu Morin c04d27265b Metadata parsing and types update 2024-04-05 14:32:07 +02:00
Matthieu Morin 22e59b089f CONTENT - structure, schema, config updates + file transfers 2024-04-05 14:29:56 +02:00
Matthieu Morin 6ac2261cf1 new Cloudinary image logic - schema, validation, 2024-04-05 14:28:25 +02:00
Matthieu Morin c78de3ec28 removing unused due to restructure 2024-04-05 14:26:06 +02:00
Matthieu Morin 0e30d0a539 Reactive SEO - Global default in root layout, reactive reusable component, data props 2024-04-03 16:36:27 +02:00
matthieu42morin 8291443c80 FOR REFERENCE unfinished imageuploader script for automating image upload, may ditch that in the future 2024-03-31 05:40:43 +02:00
matthieu42morin d73bac59a8 data pages for listing everything json, each category and each service under a category, bordel de merde for the time being 2024-03-31 05:39:48 +02:00
matthieu42morin 1998709a8a delete slug, bad naming 2024-03-31 05:37:35 +02:00
matthieu42morin 5107f756e1 update Validation to fit current structure 2024-03-31 05:36:31 +02:00
matthieu42morin ff3f943a0d utils 2024-03-31 05:35:55 +02:00
matthieu42morin 6c3c7ba724 cloudinary 2024-03-31 05:34:31 +02:00
matthieu42morin ee9b7501e2 schema upd 2024-03-31 05:34:15 +02:00
matthieu42morin e9ab134ebd sitemap 2024-03-31 05:33:43 +02:00
matthieu42morin efaae142f1 SEO 2024-03-31 05:33:09 +02:00
matthieu42morin bd515e7339 typo 2024-03-31 05:08:13 +02:00
matthieu42morin 4811bcdd23 parse formatters - frontmatter and json 2024-03-30 05:05:33 +01:00
matthieu42morin fdb2b9b4ed type changes 2024-03-30 05:04:43 +01:00
matthieu42morin c08635b4c6 deps update 2024-03-30 05:02:56 +01:00
matthieu42morin d06ed63a75 data pop update 2024-03-30 05:02:39 +01:00
matthieu42morin 948ef1ca47 Map with testing 2024-03-25 23:33:13 +01:00
matthieu42morin 1816858bc6 Config - links update 2024-03-25 23:31:44 +01:00
matthieu42morin 3db7f22a17 readme update 2024-03-24 16:09:34 +01:00
matthieu42morin 2c939ff741 mdsvex 2024-03-24 14:40:56 +01:00
matthieu42morin 8187fb58bc Accelerated Mobile Pages (AMP) 2024-03-24 14:36:58 +01:00
matthieu42morin c584fd1f29 RESTRUCTURE - deleting previous locations 2024-03-22 18:29:53 +01:00
matthieu42morin 7121aacc7e RESTRUCTURE - updating content locations 2024-03-22 18:29:10 +01:00
matthieu42morin 41e5260767 RESTRUCTURE JSON Schema 2024-03-22 18:27:39 +01:00
matthieu42morin 753fbc76b7 helper scripts 2024-03-22 15:18:46 +01:00
matthieu42morin 06d84f6d6e SVG Email obfuscation 2024-03-22 15:01:15 +01:00
matthieu42morin 75ee13fe94 remark custom plugins 2024-03-22 15:00:56 +01:00
matthieu42morin fafb0b1c1e Content update 2024-03-22 15:00:32 +01:00
matthieu42morin 40ddca238e Merge branch 'master' of https://git.mattmor.in/Madmin/KkosmetickySalon 2024-03-22 02:57:29 +01:00
matthieu42morin e0f0efb590 delete unused 2024-03-22 02:56:09 +01:00
Matthieu Morin ea479a1f4e Update README.md 2024-03-20 15:24:06 +00:00
Matthieu Morin 5bf94b1db4 Merge pull request 'Recent Overhaul to content structure, simplifying, read README' (#1) from rand into master
Reviewed-on: #1
2024-03-20 15:23:10 +00:00
81 changed files with 2075 additions and 631 deletions

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -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

73
mdsvex.config.js Normal file
View File

@ -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;

View File

@ -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"

25
scripts/uploadImage.js Normal file
View File

@ -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);
});
});

View File

@ -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" />

14
src/content/images.json Normal file
View File

@ -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" }
]
}
}

View File

@ -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#"
}
}
}
}

View File

@ -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')"
}
]
}
}
}

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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)

View File

@ -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í.

View File

@ -1,69 +0,0 @@
{
"$schema": "$routes/sluzby/schema.json",
"category": "Permanentní make-up",
"items": [
{
"name": "Obočí Pudrové, Ombré",
"description": "Diagnostika pleti, odlíčení tonizace",
"id": "oboci",
"price": 3000,
"duration": 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ě"
}
]
}

View File

@ -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čí.

View File

@ -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čí.

View File

@ -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čí.

View File

@ -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ě"
}
]
}

View File

@ -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.

View File

@ -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.

View File

@ -1,41 +0,0 @@
{
"$schema": "/src/routes/sluzby/schema.json",
"category": "Vakuslim 48 - zeštíhlující procedura",
"items": [
{
"name": "Ošetření horních končetin",
"description": "Diagnostika pleti, odlíčení tonizace",
"id": "vakuslim-48-zestihlujici-procedura-horni-koncetiny",
"price": 600,
"duration": 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
}
]
}

View File

@ -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
}
]
}

View File

@ -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,-

View File

@ -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,-

View File

@ -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
}
]
}

View File

@ -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í.

View File

@ -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`

View File

@ -1,47 +1,77 @@
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,
PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID PUBLIC_SENTRY_ORG_ID
} from '$env/static/public'; } from '$env/static/public';
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 }) => {
if (!csp) { if (!csp) {
throw new Error('csp is undefined'); throw new Error('csp is undefined');
} }
const response = await resolve(event); const response = await resolve(event);
// Permission fullscreen necessary for maps fullscreen // Permission fullscreen necessary for maps fullscreen
const headers = { const headers = {
'X-Frame-Options': 'SAMEORIGIN', 'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'no-referrer', 'Referrer-Policy': 'no-referrer',
'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`, 'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`,
'X-Content-Type-Options': 'nosniff', 'X-Content-Type-Options': 'nosniff',
// 'Content-Security-Policy-Report-Only': csp, // 'Content-Security-Policy-Report-Only': csp,
'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();

View File

@ -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>

View File

@ -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"
> >
@ -53,4 +53,4 @@
</AppBar> </AppBar>
<style lang="postcss"> <style lang="postcss">
</style> </style>

View File

@ -1,53 +1,87 @@
<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';
export let lngLat = { lng: 49.317881, lat: 14.104978 };
export let clazz = "absolute inset-0" import * as conf from '$lib/config';
let hasWebGL = false; import EmailObfuscated from './EmailObfuscated.svelte';
let isLoading = true; export let clazz = 'absolute inset-0';
onMount(async () => {
try { export let lngLat = { lng: 49.317881, lat: 14.104978 };
const canvas = document.createElement('canvas'); let hasWebGL = false;
hasWebGL = !!(window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))); let isLoading = true;
} catch (e) {
hasWebGL = false; onMount(async () => {
} try {
isLoading = false; const canvas = document.createElement('canvas');
}); hasWebGL = !!(
window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
);
} catch (e) {
hasWebGL = false;
}
isLoading = false;
});
</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}
<MapLibre <section class="relative">
center={[49.317881,14.104978]} <div class="container px-5 py-24 mx-auto flex sm:flex-nowrap flex-wrap">
zoom={2} <div
class="map" 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"
standardControls >
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json {clazz}" <MapLibre
> center={[49.317881, 14.104978]}
<Marker zoom={2}
{lngLat} class="map"
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" standardControls
> style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json {clazz}"
<span> >
KKosmetickySalon Oldřichov <Marker
</span> {lngLat}
<Popup openOn="hover" offset={[0, -10]}> 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"
<div class="text-lg font-bold">Přijeďte ke mně :)</div> >
</Popup> <span> KKosmetickySalon Oldřichov </span>
</Marker> <Popup openOn="hover" offset={[0, -10]}>
</MapLibre> <div class="text-lg font-bold">Přijeďte ke mně :)</div>
</Popup>
</Marker>
</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>
:global(.map) { :global(.map) {
height: 500px; height: 500px;
} }
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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: '/' },

View File

@ -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;

6
src/lib/types/images.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export type ImageLinkArgs = {
urlOrPublicId: string;
h: number;
w: number;
max?: boolean;
};

View File

@ -1,7 +1,7 @@
export type InstagramPost = { export interface InstagramPost {
id: string; id: string;
media_type: string; media_type: string;
media_url: string; media_url: string;
caption: string; caption: string;
timestamp: string; timestamp: string;
} }

21
src/lib/types/mdMetadata.d.ts vendored Normal file
View File

@ -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' | '';

14
src/lib/types/ogMetadata.d.ts vendored Normal file
View File

@ -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;
};

View File

@ -1,8 +0,0 @@
export type Product = {
id: string;
name: string;
price: number;
description: string;
image: string;
url: string;
}

View File

@ -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: { description: string;
name: string; id: string;
description: string; image: string;
id: 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;

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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'))

View File

@ -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;

60
src/lib/utils/helpers.ts Normal file
View File

@ -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;
};

View File

@ -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);
};

11
src/lib/utils/list.ts Normal file
View File

@ -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;
}

View File

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

View File

@ -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;
};

View File

@ -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;
};

32
src/lib/utils/scroll.ts Normal file
View File

@ -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'
});
};

View File

@ -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();

View File

@ -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&amp;layer=mapnik&amp;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&amp;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 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> </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% {

View File

@ -0,0 +1 @@
export const csr = false;

View File

@ -23,29 +23,38 @@
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: '' }, // const metaDefaults = {
// Twitter // title: 'BeautySalon',
twitter: { // description: 'BeautySalon Popis.',
title: metaDefaults.title, // image: 'https://user-images.githubusercontent.com/1509726/212382766-f29b9c9a-82e3-44c2-b911-b17a9197e5b9.jpg'
description: metaDefaults.description, // };
image: metaDefaults.image // 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
@ -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 />

View File

@ -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>

View File

@ -0,0 +1,23 @@
<script lang="ts">
</script>
<section>
<!-- Úvod -->
</section>
<section>
<!-- Foto -->
</section>
<section>
<!-- Certifikace -->
</section>
<section>
<!-- Foto -->
</section>
<section>
<!-- -->
</section>

View File

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

View File

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

View File

@ -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'
}
});
};

View File

@ -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()
// };
// };

View File

@ -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}

View File

@ -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
};
};

View File

@ -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 -->

View File

@ -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'
kit: { import preprocess from 'svelte-preprocess'
adapter: adapter({
precompress: false
}),
// Aliases need tsconfig explicit inclusion function getAdapter() {
alias: { if (Object.keys(process.env).some(key => key.includes('VERCEL'))) {
$lib: './src/lib', return adapterVercel()
$root: './', } else if (Object.keys(process.env).some(key => key.includes('NETLIFY'))) {
$src: './src', return adapterNetlify()
$routes: './src/routes', } else if (Object.keys(process.env).some(key => key.includes('CF_PAGES'))) {
$content: './src/content' return adapterCloudflare()
} else {
return process.env.ADAPTER === 'node'
? adapterNode({ out: 'build' })
: adapterStatic({
pages: 'build',
assets: 'build',
fallback: undefined
})
}
}
}, /** @type {import("@svletejs/kit".Config)} */
env: { export default {
publicPrefix: "PUBLIC_", extensions: ['.svelte', ...(mdsvexConfig.extensions || [])],
}, preprocess: [preprocess({ postcss: true }), mdsvex(mdsvexConfig) /*, vitePreprocess()*/],
}, vitePlugin: {
}; inspector: true
},
kit: {
adapter: getAdapter(),
alias: {
$lib: './src/lib',
$root: './',
$src: './src',
$routes: './src/routes',
$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
}
export default config; throw new Error(`${status} ${path} from ${referrer}, ~~~~~~~~~ message: ${message}~~~~~~~~~`)
}
}
}
}

View File

@ -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
} }
} }

View File

@ -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
});

View File

@ -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();
});

22
tests/e2e/service.test.ts Normal file
View File

@ -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
});

35
tests/validateImages.js Normal file
View File

@ -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();

View File

@ -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
// //

View File

@ -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'),
} }
} }
}); });