diff --git a/.gitignore b/.gitignore index 6cfde11..2eac44b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Affinity Designer lock files +*~lock~ diff --git a/drizzle/0001_confused_forge.sql b/drizzle/0001_confused_forge.sql new file mode 100644 index 0000000..01db809 --- /dev/null +++ b/drizzle/0001_confused_forge.sql @@ -0,0 +1,42 @@ +CREATE TABLE `banner_images` ( + `id` text PRIMARY KEY NOT NULL, + `image_path` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `news_posts` ( + `id` text PRIMARY KEY NOT NULL, + `title` text NOT NULL, + `slug` text NOT NULL, + `excerpt` text, + `body` text DEFAULT '' NOT NULL, + `cover_image_path` text, + `published` integer DEFAULT false NOT NULL, + `published_at` integer, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `news_posts_slug_unique` ON `news_posts` (`slug`);--> statement-breakpoint +CREATE TABLE `service_images` ( + `id` text PRIMARY KEY NOT NULL, + `service_id` text NOT NULL, + `image_path` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`service_id`) REFERENCES `services`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `services` ( + `id` text PRIMARY KEY NOT NULL, + `title` text NOT NULL, + `description` text, + `bullets` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL +); diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..e4485e9 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,429 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f9b24229-b6a3-481b-9bb6-5028ef961005", + "prevId": "937f8d50-c54c-47ab-8b71-933736e4cfdb", + "tables": { + "banner_images": { + "name": "banner_images", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gallery_images": { + "name": "gallery_images", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "news_posts": { + "name": "news_posts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "excerpt": { + "name": "excerpt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "cover_image_path": { + "name": "cover_image_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published": { + "name": "published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "news_posts_slug_unique": { + "name": "news_posts_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "service_images": { + "name": "service_images", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "service_images_service_id_services_id_fk": { + "name": "service_images_service_id_services_id_fk", + "tableFrom": "service_images", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "services": { + "name": "services", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bullets": { + "name": "bullets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'editor'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3aa941d..75b8228 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1785695615587, "tag": "0000_silly_bushwacker", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1785773840324, + "tag": "0001_confused_forge", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/Main.svelte b/src/lib/Main.svelte deleted file mode 100644 index 9ee9bd6..0000000 --- a/src/lib/Main.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - -
- - - - - - - - - - -
\ No newline at end of file diff --git a/src/lib/Service.svelte b/src/lib/Service.svelte deleted file mode 100644 index 5f9b473..0000000 --- a/src/lib/Service.svelte +++ /dev/null @@ -1,77 +0,0 @@ - - - - -
-
-

- {service} -

-
-
- - - -
- -
-
-
\ No newline at end of file diff --git a/src/lib/Slideshow.svelte b/src/lib/Slideshow.svelte new file mode 100644 index 0000000..87b335f --- /dev/null +++ b/src/lib/Slideshow.svelte @@ -0,0 +1,79 @@ + + +{#if images.length > 0} +
+ {#each images as src, i (src)} + + {/each} + {#if images.length > 1} +
+ {#each images as _, i (i)} + + {/each} +
+ {/if} +
+{/if} + + diff --git a/src/lib/ZoomInImage.svelte b/src/lib/ZoomInImage.svelte deleted file mode 100644 index 9e821e3..0000000 --- a/src/lib/ZoomInImage.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - - - -
- {serviceImageAlt} -
\ No newline at end of file diff --git a/src/lib/assets/gallery_icon.svg b/src/lib/assets/gallery_icon.svg new file mode 100644 index 0000000..72bbfc6 --- /dev/null +++ b/src/lib/assets/gallery_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/assets/intelidom_admin_banner.afdesign b/src/lib/assets/intelidom_admin_banner.afdesign new file mode 100644 index 0000000..12755b8 Binary files /dev/null and b/src/lib/assets/intelidom_admin_banner.afdesign differ diff --git a/src/lib/assets/intelidom_admin_banner.svg b/src/lib/assets/intelidom_admin_banner.svg new file mode 100644 index 0000000..db88c6f --- /dev/null +++ b/src/lib/assets/intelidom_admin_banner.svg @@ -0,0 +1 @@ +ADMIN \ No newline at end of file diff --git a/src/lib/server/content.ts b/src/lib/server/content.ts new file mode 100644 index 0000000..f94efe1 --- /dev/null +++ b/src/lib/server/content.ts @@ -0,0 +1,267 @@ +import { and, asc, desc, eq, ne } from 'drizzle-orm'; +import { randomUUID } from 'node:crypto'; +import { db } from './db'; +import { + bannerImages, + newsPosts, + serviceImages, + services, + settings, + type ContactInfo, + type NewsPost +} from './db/schema'; + +const nextSort = (rows: { sortOrder: number }[]) => + rows.reduce((max, r) => Math.max(max, r.sortOrder), -1) + 1; + +/** Rewrite sortOrder to match the given id order (0..n-1). */ +function applyOrder(ids: string[], table: typeof bannerImages | typeof services) { + ids.forEach((id, i) => { + db.update(table).set({ sortOrder: i }).where(eq(table.id, id)).run(); + }); +} + +// ---------- Banner ---------- + +export function listBannerImages() { + return db + .select() + .from(bannerImages) + .orderBy(asc(bannerImages.sortOrder), asc(bannerImages.createdAt)) + .all(); +} + +export function addBannerImage(imagePath: string) { + const sortOrder = nextSort(listBannerImages()); + db.insert(bannerImages).values({ id: randomUUID(), imagePath, sortOrder }).run(); +} + +export function deleteBannerImage(id: string): string | null { + const row = db.select().from(bannerImages).where(eq(bannerImages.id, id)).get(); + if (!row) return null; + db.delete(bannerImages).where(eq(bannerImages.id, id)).run(); + return row.imagePath; +} + +export function moveBanner(id: string, dir: 'up' | 'down') { + const rows = listBannerImages(); + const idx = rows.findIndex((r) => r.id === id); + const swap = dir === 'up' ? idx - 1 : idx + 1; + if (idx < 0 || swap < 0 || swap >= rows.length) return; + const ids = rows.map((r) => r.id); + [ids[idx], ids[swap]] = [ids[swap], ids[idx]]; + applyOrder(ids, bannerImages); +} + +// ---------- Services ---------- + +export function listServices() { + const rows = db + .select() + .from(services) + .orderBy(asc(services.sortOrder), asc(services.createdAt)) + .all(); + return rows.map((s) => ({ ...s, images: listServiceImages(s.id) })); +} + +export function getService(id: string) { + const s = db.select().from(services).where(eq(services.id, id)).get(); + if (!s) return null; + return { ...s, images: listServiceImages(id) }; +} + +export function listServiceImages(serviceId: string) { + return db + .select() + .from(serviceImages) + .where(eq(serviceImages.serviceId, serviceId)) + .orderBy(asc(serviceImages.sortOrder)) + .all(); +} + +export function createService(input: { + title: string; + description: string | null; + bullets: string | null; +}): string { + const id = randomUUID(); + const sortOrder = nextSort( + db.select({ sortOrder: services.sortOrder }).from(services).all() + ); + db.insert(services).values({ id, sortOrder, ...input }).run(); + return id; +} + +export function updateService( + id: string, + input: { title: string; description: string | null; bullets: string | null } +) { + db.update(services).set(input).where(eq(services.id, id)).run(); +} + +export function deleteService(id: string): string[] { + const imgs = listServiceImages(id).map((i) => i.imagePath); + db.delete(services).where(eq(services.id, id)).run(); // cascades to service_images rows + return imgs; +} + +export function moveService(id: string, dir: 'up' | 'down') { + const rows = db.select().from(services).orderBy(asc(services.sortOrder), asc(services.createdAt)).all(); + const idx = rows.findIndex((r) => r.id === id); + const swap = dir === 'up' ? idx - 1 : idx + 1; + if (idx < 0 || swap < 0 || swap >= rows.length) return; + const ids = rows.map((r) => r.id); + [ids[idx], ids[swap]] = [ids[swap], ids[idx]]; + applyOrder(ids, services); +} + +export function addServiceImage(serviceId: string, imagePath: string) { + const sortOrder = nextSort(listServiceImages(serviceId)); + db.insert(serviceImages).values({ id: randomUUID(), serviceId, imagePath, sortOrder }).run(); +} + +export function deleteServiceImage(id: string): string | null { + const row = db.select().from(serviceImages).where(eq(serviceImages.id, id)).get(); + if (!row) return null; + db.delete(serviceImages).where(eq(serviceImages.id, id)).run(); + return row.imagePath; +} + +// ---------- News ---------- + +function slugify(input: string): string { + return input + .toLowerCase() + .replace(/č/g, 'c') + .replace(/š/g, 's') + .replace(/ž/g, 'z') + .replace(/đ/g, 'd') + .replace(/ć/g, 'c') + .normalize('NFKD') + .replace(/[̀-ͯ]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function uniqueSlug(base: string, excludeId?: string): string { + const root = slugify(base) || 'objava'; + let slug = root; + let n = 1; + while (true) { + const clash = db + .select({ id: newsPosts.id }) + .from(newsPosts) + .where(excludeId ? and(eq(newsPosts.slug, slug), ne(newsPosts.id, excludeId)) : eq(newsPosts.slug, slug)) + .get(); + if (!clash) return slug; + n += 1; + slug = `${root}-${n}`; + } +} + +export function listPosts(publishedOnly: boolean): NewsPost[] { + const rows = db.select().from(newsPosts).all(); + const visible = publishedOnly ? rows.filter((p) => p.published) : rows; + return visible.sort((a, b) => { + const at = (a.publishedAt ?? a.createdAt).getTime(); + const bt = (b.publishedAt ?? b.createdAt).getTime(); + return bt - at; + }); +} + +export function getPost(id: string) { + return db.select().from(newsPosts).where(eq(newsPosts.id, id)).get(); +} + +export function getPublishedPostBySlug(slug: string) { + const p = db.select().from(newsPosts).where(eq(newsPosts.slug, slug)).get(); + return p && p.published ? p : null; +} + +export function createPost(input: { + title: string; + excerpt: string | null; + body: string; + coverImagePath: string | null; + published: boolean; +}): string { + const id = randomUUID(); + const now = new Date(); + db.insert(newsPosts) + .values({ + id, + title: input.title, + slug: uniqueSlug(input.title), + excerpt: input.excerpt, + body: input.body, + coverImagePath: input.coverImagePath, + published: input.published, + publishedAt: input.published ? now : null, + createdAt: now, + updatedAt: now + }) + .run(); + return id; +} + +export function updatePost( + id: string, + input: { + title: string; + excerpt: string | null; + body: string; + coverImagePath?: string | null; + published: boolean; + } +) { + const existing = getPost(id); + if (!existing) return; + db.update(newsPosts) + .set({ + title: input.title, + slug: uniqueSlug(input.title, id), + excerpt: input.excerpt, + body: input.body, + ...(input.coverImagePath !== undefined ? { coverImagePath: input.coverImagePath } : {}), + published: input.published, + // Set publishedAt the first time it becomes published. + publishedAt: input.published ? (existing.publishedAt ?? new Date()) : null, + updatedAt: new Date() + }) + .where(eq(newsPosts.id, id)) + .run(); +} + +export function deletePost(id: string): string | null { + const row = getPost(id); + if (!row) return null; + db.delete(newsPosts).where(eq(newsPosts.id, id)).run(); + return row.coverImagePath; +} + +// ---------- Settings / contact ---------- + +const DEFAULT_CONTACT: ContactInfo = { + email: 'info@intelidom.si', + phone: '040 77 11 66', + address: '', + showMap: false +}; + +export function getContact(): ContactInfo { + const row = db.select().from(settings).where(eq(settings.key, 'contact')).get(); + if (!row) return DEFAULT_CONTACT; + try { + return { ...DEFAULT_CONTACT, ...(JSON.parse(row.value) as Partial) }; + } catch { + return DEFAULT_CONTACT; + } +} + +export function setContact(info: ContactInfo) { + const value = JSON.stringify(info); + db.insert(settings) + .values({ key: 'contact', value }) + .onConflictDoUpdate({ target: settings.key, set: { value } }) + .run(); +} diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index e1debae..78bc0d5 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -35,9 +35,78 @@ export const galleryImages = sqliteTable('gallery_images', { .$defaultFn(() => new Date()) }); +/** Hero banner slideshow images shown at the top of the homepage. */ +export const bannerImages = sqliteTable('banner_images', { + id: text('id').primaryKey(), + imagePath: text('image_path').notNull(), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .notNull() + .$defaultFn(() => new Date()) +}); + +/** Services shown on the homepage (name + description + bullet list + images). */ +export const services = sqliteTable('services', { + id: text('id').primaryKey(), + title: text('title').notNull(), + description: text('description'), + // One bullet per line; rendered as a list. + bullets: text('bullets'), + sortOrder: integer('sort_order').notNull().default(0), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .notNull() + .$defaultFn(() => new Date()) +}); + +/** Images belonging to a service; multiple rotate as a slideshow. */ +export const serviceImages = sqliteTable('service_images', { + id: text('id').primaryKey(), + serviceId: text('service_id') + .notNull() + .references(() => services.id, { onDelete: 'cascade' }), + imagePath: text('image_path').notNull(), + sortOrder: integer('sort_order').notNull().default(0) +}); + +/** Blog / news posts. */ +export const newsPosts = sqliteTable('news_posts', { + id: text('id').primaryKey(), + title: text('title').notNull(), + slug: text('slug').notNull().unique(), + excerpt: text('excerpt'), + body: text('body').notNull().default(''), + coverImagePath: text('cover_image_path'), + published: integer('published', { mode: 'boolean' }).notNull().default(false), + publishedAt: integer('published_at', { mode: 'timestamp_ms' }), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .notNull() + .$defaultFn(() => new Date()) +}); + +/** Simple key/value store for site settings (contact info lives under key "contact"). */ +export const settings = sqliteTable('settings', { + key: text('key').primaryKey(), + value: text('value').notNull() +}); + export type User = typeof users.$inferSelect; export type Session = typeof sessions.$inferSelect; export type GalleryImage = typeof galleryImages.$inferSelect; +export type BannerImage = typeof bannerImages.$inferSelect; +export type Service = typeof services.$inferSelect; +export type ServiceImage = typeof serviceImages.$inferSelect; +export type NewsPost = typeof newsPosts.$inferSelect; /** User shape safe to expose to the browser (no password hash). */ export type PublicUser = Pick; + +/** Contact settings stored (as JSON) under the "contact" settings key. */ +export type ContactInfo = { + email: string; + phone: string; + address: string; + showMap: boolean; +}; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..51848db --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,29 @@ +import type { PageServerLoad } from './$types'; +import { getContact, listBannerImages, listPosts, listServices } from '$lib/server/content'; + +export const load: PageServerLoad = () => { + const banner = listBannerImages().map((i) => `/uploads/${i.imagePath}`); + + const services = listServices().map((s) => ({ + id: s.id, + title: s.title, + description: s.description, + bullets: (s.bullets ?? '') + .split('\n') + .map((b) => b.trim()) + .filter(Boolean), + images: s.images.map((im) => `/uploads/${im.imagePath}`) + })); + + const posts = listPosts(true) + .slice(0, 3) + .map((p) => ({ + slug: p.slug, + title: p.title, + excerpt: p.excerpt, + coverUrl: p.coverImagePath ? `/uploads/${p.coverImagePath}` : null, + date: (p.publishedAt ?? p.createdAt).toISOString() + })); + + return { banner, services, posts, contact: getContact() }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2c57224..737d48c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,272 @@ -
+{#if data.banner.length > 0} +
+ +
+{/if} + +
+ {#if data.services.length > 0} +
+ {#each data.services as service, i (service.id)} +
+ {#if service.images.length > 0} +
+ +
+ {/if} +
+

{service.title}

+ {#if service.description}

{service.description}

{/if} + {#if service.bullets.length > 0} +
    + {#each service.bullets as bullet}
  • {bullet}
  • {/each} +
+ {/if} +
+
+ {/each} +
+ {/if} + + {#if data.posts.length > 0} +
+

Novice

+ + Vse novice → +
+ {/if} + +
+

Kontakt

+
+
+ {#if data.contact.address} +

{data.contact.address}

+ {/if} + {#if data.contact.email} +

{data.contact.email}

+ {/if} + {#if data.contact.phone} +

{data.contact.phone}

+ {/if} +
+ {#if data.contact.showMap && data.contact.address} +
+ +
+ {/if} +
+
+
+ + diff --git a/src/routes/admin/(dashboard)/+layout.svelte b/src/routes/admin/(dashboard)/+layout.svelte index 196be51..3dfb5a6 100644 --- a/src/routes/admin/(dashboard)/+layout.svelte +++ b/src/routes/admin/(dashboard)/+layout.svelte @@ -2,12 +2,13 @@ import { page } from '$app/state'; import type { Snippet } from 'svelte'; import type { LayoutData } from './$types'; - import intelidom_banner from '$lib/assets/intelidom_banner.svg'; + import intelidom_admin_banner from '$lib/assets/intelidom_admin_banner.svg'; let { data, children }: { data: LayoutData; children: Snippet } = $props(); const tabs = [ { href: '/admin/gallery', label: 'Galerija' }, + { href: '/admin/banner', label: 'Pasica' }, { href: '/admin/services', label: 'Storitve' }, { href: '/admin/news', label: 'Novice' }, { href: '/admin/contact', label: 'Kontakt' } @@ -24,8 +25,7 @@