Merge branch 'master' of https://git.mattmor.in/Madmin/KkosmetickySalon
This commit is contained in:
commit
e6203baf9e
|
@ -4,7 +4,9 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"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",
|
||||
"build": "pnpm run validate && vite build",
|
||||
"build-dev": "pnpm run validate && vite build --mode development",
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Service category schema",
|
||||
"type": "object",
|
||||
"required": ["title", "description", "id", "image", "services"],
|
||||
"required": ["title", "description", "id", "services"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
|
@ -26,8 +26,7 @@
|
|||
"image": {
|
||||
"title": "Image",
|
||||
"description": "A featured image in previews and on top of page",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"title": "tags",
|
||||
|
@ -41,7 +40,7 @@
|
|||
"title": "services under the category",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./schema-services.json"
|
||||
"$ref": "src/content/schema-services.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Service schema",
|
||||
"type": "object",
|
||||
"required": ["title", "description", "id", "image", "duration", "price"],
|
||||
"required": ["title", "description", "id", "duration", "price"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
|
@ -24,8 +24,7 @@
|
|||
"image": {
|
||||
"title": "Image",
|
||||
"description": "A featured image in previews and on top of page",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
"title": "Price",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema-categories.json",
|
||||
"$schema": "../../schema-categories.json",
|
||||
"title": "DALŠÍ VELMI OBLÍBENÉ SLUŽBY",
|
||||
"description": "A description of a description",
|
||||
"id": "depilace",
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema-categories.json",
|
||||
"$schema": "../../schema-categories.json",
|
||||
"title": "Depilace",
|
||||
"description": "A description of a description",
|
||||
"id": "depilace",
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema-categories.json",
|
||||
"$schema": "../../schema-categories.json",
|
||||
"title": "Kosmetické ošetření",
|
||||
"description": "A description of a description",
|
||||
"id": "depilace",
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema-categories.json",
|
||||
"$schema": "../../schema-categories.json",
|
||||
"title": "Permanentní make-up",
|
||||
"description": "A description of a description",
|
||||
"id": "pmu",
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "../schema-categories.json",
|
||||
"$schema": "../../schema-categories.json",
|
||||
"title": "Vakuslim 48 - zeštíhlující procedura",
|
||||
"description": "A description of a description",
|
||||
"id": "depilace",
|
|
@ -1,12 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type Product
|
||||
</script>
|
||||
|
||||
<container class="container">
|
||||
<section>
|
||||
<header>
|
||||
<img class="w-max h-max" src=`${product.image}` alt=`${product.name}`>
|
||||
</header>
|
||||
|
||||
</section>
|
||||
</container>
|
|
@ -3,26 +3,27 @@
|
|||
import type { Service } from '$lib/types/service';
|
||||
import convertMinutesToHours from '$lib/utils/minToH';
|
||||
|
||||
export let item: Service['items'][number];
|
||||
export let service: Service[][number];
|
||||
|
||||
</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>
|
||||
<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>
|
||||
<!-- <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-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>
|
||||
<article>
|
||||
<p class="text-gray-800 font-medium hidden md:block mb-4">
|
||||
{item.description}
|
||||
{service.description}
|
||||
</p>
|
||||
<a
|
||||
href="/sluzby/{item.id}"
|
||||
href="/sluzby/{service.id}"
|
||||
class="text-primary-600 hover:text-primary-800 underline mb-2 md:mb-0">
|
||||
...Zjistěte více
|
||||
</a>
|
||||
|
@ -32,11 +33,11 @@
|
|||
<footer class="justify-self-end align-bottom">
|
||||
<div class="flex flex-col md:flex-row md:justify-between items-center">
|
||||
<p class=" text-lg text-surface-900 font-semibold text-right">
|
||||
{typeof item.price === 'number' ? `${item.price},-` : item.price}
|
||||
{typeof item.duration === 'number' ? `/${convertMinutesToHours(item.duration)}` : ''}
|
||||
{typeof service.price === 'number' ? `${service.price},-` : service.price}
|
||||
{typeof service.duration === 'number' ? `/${convertMinutesToHours(service.duration)}` : ''}
|
||||
</p>
|
||||
<a
|
||||
href="https://app.cal.com/kkosmetickysalon/{item.id}"
|
||||
href="https://app.cal.com/kkosmetickysalon/{service.id}"
|
||||
class="btn btn-md variant-filled-primary">
|
||||
Rezervace
|
||||
</a>
|
||||
|
|
|
@ -1,43 +1,30 @@
|
|||
|
||||
<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 { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
|
||||
|
||||
export let services: Service[] = [];
|
||||
export let category: Category[] = [];
|
||||
</script>
|
||||
|
||||
|
||||
<section>
|
||||
<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>
|
||||
<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 slot="summary">
|
||||
<p class="text-lg font-bold">{service.category}</p>
|
||||
<p class="text-base">{c.description}</p>
|
||||
</svelte:fragment>
|
||||
<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">
|
||||
{#each service.items as item}
|
||||
<ServiceCard {item} />
|
||||
{#each c.services as service}
|
||||
<ServiceCard {service} />
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AccordionItem>
|
||||
{/each}
|
||||
</Accordion>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
</section>
|
||||
|
|
|
@ -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>
|
|
@ -10,5 +10,5 @@ export interface OpenGraphMetadata {
|
|||
imageType: OGImageType;
|
||||
imageWidth: OGImageWidth;
|
||||
imageHeight: OGImageHeight;
|
||||
url: string;
|
||||
canonical: string;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { MarkdownMetadata } from '$lib/types/mdMetadata';
|
|||
import type { OpenGraphMetadata } from '$lib/types/ogMetadata';
|
||||
|
||||
// Base service item type
|
||||
export interface Service {
|
||||
export type Service = {
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
|
|
|
@ -40,30 +40,3 @@ export const readJsonFile = async (filePath: string) => {
|
|||
const jsonData = await fs.readFileSync(path.join(process.cwd(), 'src', 'content', filePath), 'utf-8');
|
||||
return JSON.parse(jsonData);
|
||||
}
|
||||
|
||||
// ======= MARKDOWN PARSER ========
|
||||
|
||||
// https://github.com/jonschlinkert/gray-matter
|
||||
import * as matter from 'gray-matter';
|
||||
// https://github.com/markedjs/marked
|
||||
// import marked from 'marked';
|
||||
|
||||
export const parseMarkdownFile = async (filePath: string) => {
|
||||
const markdownData = await 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,
|
||||
// };
|
||||
// }
|
||||
// export function parseMarkdownFile(filePath: string) {
|
||||
// const markdownData = fs.readFileSync(path.join(process.cwd(), filePath), 'utf-8');
|
||||
// const { data, content } = grayMatter(markdownData);
|
||||
|
||||
// return { frontmatter: data, content };
|
||||
// }
|
||||
|
||||
console.log(parseMarkdownFile('../../content/permanentni-make-up/pmu/pmu.md'))
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// ======= MARKDOWN PARSER ========
|
||||
|
||||
// https://github.com/jonschlinkert/gray-matter
|
||||
import matter from 'gray-matter';
|
||||
// https://github.com/markedjs/marked;- unused
|
||||
// import marked from 'marked';
|
||||
import fs from 'fs';
|
||||
import path from 'path'
|
||||
|
||||
export const parseMarkdownFile = async (filePath: string) => {
|
||||
const markdownData = fs.readFileSync(path.join(process.cwd(), 'src', 'content', filePath), 'utf-8');
|
||||
const { data, content } = matter(markdownData);
|
||||
return { frontmatter: data, content };
|
||||
}
|
||||
// export function parseMarkdown<T>(filePath: string): { frontmatter: T; content: string } {
|
||||
// const data = matter.read(filePath).data;
|
||||
// return {
|
||||
// frontmatter: data as T,
|
||||
// };
|
||||
// }
|
||||
console.log(parseMarkdownFile('../../content/permanentni-make-up/pmu/pmu.md'))
|
|
@ -0,0 +1,21 @@
|
|||
// imageService.ts
|
||||
import cloudinary from 'cloudinary';
|
||||
import imagesData from '$content/images.json';
|
||||
|
||||
cloudinary.v2.config({
|
||||
cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
|
||||
});
|
||||
|
||||
export const getCloudinaryImageUrl = (publicId: string, options: cloudinary.UploadApiOptions = {}) => {
|
||||
const imageData = imagesData[publicId];
|
||||
if (!imageData) {
|
||||
throw new Error(`Image with public ID ${publicId} not found in images.json`);
|
||||
}
|
||||
|
||||
const transformationOptions = {
|
||||
...options,
|
||||
...imageData.transformations,
|
||||
};
|
||||
|
||||
return cloudinary.v2.url(publicId, transformationOptions);
|
||||
};
|
|
@ -0,0 +1,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();
|
|
@ -0,0 +1,3 @@
|
|||
<script lang="ts">
|
||||
|
||||
</script>
|
|
@ -1,73 +1,17 @@
|
|||
<script lang="ts">
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import ServicesLayout from '$lib/components/services/ServicesLayout.svelte';
|
||||
import type { Service } from '$lib/types/service';
|
||||
export const id = '';
|
||||
|
||||
// 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 }
|
||||
]
|
||||
}
|
||||
];
|
||||
export let services: Service[] = []
|
||||
</script>
|
||||
|
||||
{#if props.services.length === 0}
|
||||
<Spinner/>
|
||||
<p>Loading services...</p>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center py-8">
|
||||
<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>
|
||||
<ServicesLayout {services} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!-- src/routes/[categoryId]/[serviceId]/+page.svelte -->
|
||||
<script lang="ts">
|
||||
import * as conf from '$lib/config'
|
||||
import type { PageData } from './$types';
|
||||
import { SEO } from '$lib/components/SEO.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<SEO seoData={data}/>
|
||||
|
||||
<!-- Your service content goes here -->
|
|
@ -1,31 +0,0 @@
|
|||
import type { ExtendedCategory } from '$lib/types';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { parseMarkdown } from '$lib/markdownParser';
|
||||
import type { ExtendedCategory } from '$lib/types';
|
||||
|
||||
export async function get({ params }) {
|
||||
const { category, service } = params;
|
||||
|
||||
// Read the JSON file for the service
|
||||
const jsonPath = path.join('src', 'content', category, service, `${service}.json`);
|
||||
const jsonContent = await fs.readFile(jsonPath, 'utf-8');
|
||||
const serviceData = JSON.parse(jsonContent);
|
||||
|
||||
// Read the markdown file for the service
|
||||
const markdownPath = path.join('src', 'content', category, service, `${service}.md`);
|
||||
const markdownContent = await fs.readFile(markdownPath, 'utf-8');
|
||||
const { frontmatter, headings } = parseMarkdown(markdownContent);
|
||||
|
||||
// Combine the service data with the frontmatter and headings to create the extended service
|
||||
const ExtendedCategory: ExtendedCategory = {
|
||||
...serviceData,
|
||||
...frontmatter,
|
||||
headings,
|
||||
};
|
||||
|
||||
return {
|
||||
body: ExtendedCategory,
|
||||
};
|
||||
};
|
|
@ -1,134 +0,0 @@
|
|||
|
||||
// import type { PageLoad } from './$types';
|
||||
// import servicesData from '$lib/services.json'; // Assuming you have a JSON file with all the data
|
||||
|
||||
// export const load: PageLoad = async ({ params }) => {
|
||||
// const { category, service } = params;
|
||||
// const serviceItem = servicesData.find((s) => s.category === category)?.items.find((item) => item.id === service);
|
||||
|
||||
// if (!serviceItem) {
|
||||
// throw new Error('Service not found');
|
||||
// }
|
||||
|
||||
// return {
|
||||
// props: {
|
||||
// openGraphData: {
|
||||
// title: serviceItem.title,
|
||||
// description: serviceItem.description,
|
||||
// image: serviceItem.image,
|
||||
// // ...other OpenGraph data...
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// };
|
||||
// import { promises as fs } from 'fs';
|
||||
// import path from 'path';
|
||||
// import type { PageServerLoad } from './$types';
|
||||
// import { parseMarkdown } from '$lib/markdownParser'; // You'll need to create this
|
||||
// import servicesData from '$lib/services.json';
|
||||
// import type { ExtendedServiceItem, MarkdownFrontmatter } from '$lib/types';
|
||||
|
||||
// export const load: PageServerLoad = async ({ params }) => {
|
||||
// const { category, service } = params;
|
||||
|
||||
// // Read the markdown file for the service with grey matter
|
||||
// const markdownPath = path.join('src', 'content', 'posts', category, service, 'content.md');
|
||||
// const markdownContent = await fs.readFile(markdownPath, 'utf-8');
|
||||
// const { frontmatter } = parseMarkdown<MarkdownFrontmatter>(markdownContent);
|
||||
|
||||
// // Find the service item in the JSON data
|
||||
// const serviceItem = servicesData.find((s) => s.category === category)?.items.find((item) => item.id === service);
|
||||
|
||||
// if (!serviceItem) {
|
||||
// throw new Error('Service not found');
|
||||
// }
|
||||
|
||||
// // Combine the service item with the frontmatter to create the extended service item
|
||||
// const extendedServiceItem: ExtendedServiceItem = {
|
||||
// ...serviceItem,
|
||||
// ...frontmatter,
|
||||
// };
|
||||
|
||||
// return {
|
||||
// props: {
|
||||
// openGraphData: extendedServiceItem,
|
||||
// },
|
||||
// };
|
||||
// };
|
||||
|
||||
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import * as conf from '$lib/config'
|
||||
import { readJsonFile, parseMarkdownFile } from '$lib/utils';
|
||||
import { parseMarkdown } from '$lib/markdownParser';
|
||||
import type { ExtendedService } from '$lib/types';
|
||||
|
||||
export const load:PageLoad = async ({ params }) => {
|
||||
const { category, service } = params;
|
||||
|
||||
// Read the JSON file for the service
|
||||
const categoryJSON = readJsonFile(`${params.category}/${params.category}.json`);
|
||||
const serviceData = categoryJSON.find(service => service.id === params.service);
|
||||
|
||||
if (!service) {
|
||||
throw new Error(`Service not found: ${params.service}`);
|
||||
}
|
||||
|
||||
// Read the markdown file for the service
|
||||
const markdownData = parseMarkdownFile(`${params.category}/${params.service}/${params.service}.md`);
|
||||
const markdownPath = path.join('src', 'content', category, service, `${service}.md`);
|
||||
const markdownContent = await fs.readFile(markdownPath, 'utf-8');
|
||||
const frontmatter = parseMarkdown(markdownContent);
|
||||
// headings here instead of remark plugin custom?
|
||||
// Combine the service data with the frontmatter and headings to create the extended service
|
||||
const extendedService: ExtendedService = {
|
||||
...serviceData,
|
||||
...frontmatter,
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
props: {
|
||||
extendedService,
|
||||
og: {
|
||||
title: extendedService.title,
|
||||
type: 'article',
|
||||
image: extendedService.image,
|
||||
url: `https://yourwebsite.com/services/${params.category}/${params.service}`,
|
||||
description: extendedService.description,
|
||||
published_time: extendedService.frontmatter.date,
|
||||
tags: extendedService.frontmatter.tags
|
||||
}}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export async function load({ params }) {
|
||||
const category = readJsonFile(`${params.category}/pmu.json`);
|
||||
const service = category.services.find(service => service.id === params.service);
|
||||
|
||||
if (!service) {
|
||||
throw new Error(`Service not found: ${params.service}`);
|
||||
}
|
||||
|
||||
const markdownData = parseMarkdownFile(`${params.category}/${params.service}/${params.service}.md`);
|
||||
const post = { ...service, ...markdownData };
|
||||
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
og: {
|
||||
title: post.title,
|
||||
type: 'article',
|
||||
image: post.image,
|
||||
url: `${conf.url}/${params.category}/${params.service}`,
|
||||
description: post.description,
|
||||
published_time: post.frontmatter.date,
|
||||
tags: post.frontmatter.tags
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -28,6 +28,7 @@ const config = {
|
|||
$root: './',
|
||||
$src: './src',
|
||||
$routes: './src/routes',
|
||||
$content: './src/content'
|
||||
},
|
||||
env: {
|
||||
publicPrefix: "PUBLIC_",
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
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 = [];
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
validateImages();
|
|
@ -3,12 +3,17 @@
|
|||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": 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
|
||||
"noEmit": true,
|
||||
"target": "ES2018",
|
||||
|
@ -26,8 +31,9 @@
|
|||
"./src/**/*.ts",
|
||||
".svelte-kit/ambient.d.ts",
|
||||
".svelte-kit/types/**/$types.d.ts",
|
||||
"./csp-directives.ts"
|
||||
],
|
||||
"./csp-directives.ts",
|
||||
"tests/**/*",
|
||||
"src/content/**/*"],
|
||||
"exclude": ["node_modules/*"]
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
|
|
Loading…
Reference in New Issue