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 @@
-
-
-
-
-
-
-
- - Inštalacija in zagon Loxone sistema za pametno hišo
- - Montaža in zagon domofonskih sistemov
- - Odpiranje vrat na pin kodo ali kartico
-
-
-
-
- - Napeljava podatkovnih kablov za računalniška omrežja
- - Montaža komunikacijskih omaric
- - Urejanje komunikacijskih vozlišč
- - Montaža in konfiguracija WiFi dostopnih točk
-
-
-
-
- - Napeljava električnih inštalacij
- - Zamenjava dotrajanih inštalacij
- - Zamenjava rezdelilnih omaric, varovalk, vtičnic in stikal
- - Montaža luči
-
-
-
-
\ 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 @@
-
-
-
-
-
\ 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 @@
-
-
-
-
-
-

-
\ 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 @@
+
\ 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}
+
+ {/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 @@