This commit is contained in:
matthieu42morin 2024-04-05 14:41:04 +02:00
commit e6203baf9e
33 changed files with 207 additions and 353 deletions

View File

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

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

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"$schema": "../schema-categories.json",
"$schema": "../../schema-categories.json",
"title": "Depilace",
"description": "A description of a description",
"id": "depilace",

View File

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

View File

@ -1,5 +1,5 @@
{
"$schema": "../schema-categories.json",
"$schema": "../../schema-categories.json",
"title": "Permanentní make-up",
"description": "A description of a description",
"id": "pmu",

View File

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

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

View File

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

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

@ -10,5 +10,5 @@ export interface OpenGraphMetadata {
imageType: OGImageType;
imageWidth: OGImageWidth;
imageHeight: OGImageHeight;
url: string;
canonical: string;
};

View File

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

View File

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

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

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

@ -0,0 +1,3 @@
<script lang="ts">
</script>

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ const config = {
$root: './',
$src: './src',
$routes: './src/routes',
$content: './src/content'
},
env: {
publicPrefix: "PUBLIC_",

32
tests/validateImages.js Normal file
View File

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

View File

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