Service UX: images in create form, original block look + hover zoom, styled file input

- Add image picker to the "Dodaj storitev" form (upload images while creating)
- Restore original service layout on the homepage: centered uppercase title
  above, rounded image beside text, alternating sides; image zooms on hover
- Slideshow gains a hoverZoom option (scale on hover)
- Style native file inputs (::file-selector-button) to match the dashboard UI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matic Ivešić 2026-08-03 18:44:02 +02:00
parent 7ff2ca667a
commit a6076fbbf3
5 changed files with 117 additions and 35 deletions

View File

@ -92,6 +92,29 @@ body {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
color: #e5e4ea; color: #e5e4ea;
} }
/* Style the native file input to match the rest of the dashboard UI. */
input[type="file"] {
color: #9aa0a6;
font-family: inherit;
font-size: 0.9em;
}
input[type="file"]::file-selector-button {
background: rgba(255, 255, 255, 0.08);
color: #e5e4ea;
border: none;
border-radius: 8px;
padding: 0.5em 1em;
margin-right: 0.9em;
font-family: inherit;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: background-color 0.15s ease;
}
input[type="file"]::file-selector-button:hover {
background: rgba(66, 175, 56, 0.25);
}
.admin-msg { .admin-msg {
margin: 0; margin: 0;
font-size: 0.9em; font-size: 0.9em;

View File

@ -4,8 +4,15 @@
images = [], images = [],
interval = 4000, interval = 4000,
alt = '', alt = '',
fit = 'cover' fit = 'cover',
}: { images: string[]; interval?: number; alt?: string; fit?: 'cover' | 'contain' } = $props(); hoverZoom = false
}: {
images: string[];
interval?: number;
alt?: string;
fit?: 'cover' | 'contain';
hoverZoom?: boolean;
} = $props();
let current = $state(0); let current = $state(0);
@ -19,7 +26,7 @@
</script> </script>
{#if images.length > 0} {#if images.length > 0}
<div class="slideshow"> <div class="slideshow" class:zoom={hoverZoom}>
{#each images as src, i (src)} {#each images as src, i (src)}
<img {src} {alt} class:active={i === current % images.length} style:object-fit={fit} loading="lazy" /> <img {src} {alt} class:active={i === current % images.length} style:object-fit={fit} loading="lazy" />
{/each} {/each}
@ -47,13 +54,17 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0; opacity: 0;
transition: opacity 0.8s ease; transition: opacity 0.8s ease, transform 0.35s ease;
} }
.slideshow img.active { .slideshow img.active {
opacity: 1; opacity: 1;
} }
.slideshow.zoom:hover img {
transform: scale(1.1);
}
.dots { .dots {
position: absolute; position: absolute;
bottom: 0.8em; bottom: 0.8em;

View File

@ -19,14 +19,15 @@
{#if data.services.length > 0} {#if data.services.length > 0}
<section class="services" id="storitve"> <section class="services" id="storitve">
{#each data.services as service, i (service.id)} {#each data.services as service, i (service.id)}
<article class="service" class:reverse={i % 2 === 1}> <article class="service">
<h2 class="service-title">{service.title}</h2>
<div class="service-content" class:reverse={i % 2 === 1}>
{#if service.images.length > 0} {#if service.images.length > 0}
<div class="service-media"> <div class="service-media">
<Slideshow images={service.images} alt={service.title} /> <Slideshow images={service.images} alt={service.title} hoverZoom />
</div> </div>
{/if} {/if}
<div class="service-text"> <div class="service-text">
<h2>{service.title}</h2>
{#if service.description}<p class="desc">{service.description}</p>{/if} {#if service.description}<p class="desc">{service.description}</p>{/if}
{#if service.bullets.length > 0} {#if service.bullets.length > 0}
<ul> <ul>
@ -34,6 +35,7 @@
</ul> </ul>
{/if} {/if}
</div> </div>
</div>
</article> </article>
{/each} {/each}
</section> </section>
@ -104,34 +106,46 @@
.services { .services {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2.5em; gap: 3.5em;
} }
.service { .service {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5em;
align-items: center; align-items: center;
gap: 1.5em;
}
.service-title {
text-transform: uppercase;
text-align: center;
font-size: clamp(1.5em, 4vw, 2em);
margin: 0;
}
.service-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 2em;
} }
.service-media { .service-media {
width: 100%; width: 100%;
aspect-ratio: 4 / 3; aspect-ratio: 16 / 10;
border-radius: 12px; border-radius: 2em;
overflow: hidden; overflow: hidden;
background: #11161a; background: #11161a;
} }
.service-text { .service-text {
width: 100%; width: 100%;
} font-size: 1.3em;
color: #bcc0c2;
.service-text h2 {
margin: 0 0 0.4em;
} }
.desc { .desc {
color: #c7c9cf;
margin: 0 0 0.8em; margin: 0 0 0.8em;
} }
@ -142,7 +156,6 @@
.service-text li { .service-text li {
margin-bottom: 0.5em; margin-bottom: 0.5em;
color: #c7c9cf;
} }
.section-title { .section-title {
@ -251,16 +264,17 @@
} }
@media (min-width: 800px) { @media (min-width: 800px) {
.service { .service-content {
flex-direction: row; flex-direction: row;
align-items: center;
} }
.service.reverse { .service-content.reverse {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.service-media { .service-media {
width: 48%; width: 50%;
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -1,7 +1,13 @@
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { deleteUpload } from '$lib/server/storage'; import { deleteUpload, saveImage } from '$lib/server/storage';
import { createService, deleteService, listServices, moveService } from '$lib/server/content'; import {
addServiceImage,
createService,
deleteService,
listServices,
moveService
} from '$lib/server/content';
export const load: PageServerLoad = () => ({ export const load: PageServerLoad = () => ({
services: listServices().map((s) => ({ services: listServices().map((s) => ({
@ -17,11 +23,17 @@ export const actions: Actions = {
const form = await request.formData(); const form = await request.formData();
const title = String(form.get('title') ?? '').trim(); const title = String(form.get('title') ?? '').trim();
if (!title) return fail(400, { error: 'Vnesite naziv storitve.' }); if (!title) return fail(400, { error: 'Vnesite naziv storitve.' });
createService({ const id = createService({
title, title,
description: String(form.get('description') ?? '').trim() || null, description: String(form.get('description') ?? '').trim() || null,
bullets: String(form.get('bullets') ?? '').trim() || null bullets: String(form.get('bullets') ?? '').trim() || null
}); });
const files = form
.getAll('images')
.filter((e): e is File => e instanceof File && e.size > 0);
for (const file of files) {
addServiceImage(id, await saveImage('services', file));
}
return { success: 'Storitev dodana.' }; return { success: 'Storitev dodana.' };
}, },

View File

@ -3,12 +3,28 @@
import type { PageData, ActionData } from './$types'; import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
let creating = $state(false);
let fileInput: HTMLInputElement | undefined = $state();
</script> </script>
<section class="admin-section"> <section class="admin-section">
<h1>Storitve</h1> <h1>Storitve</h1>
<form class="admin-card" method="POST" action="?/create" use:enhance> <form
class="admin-card"
method="POST"
action="?/create"
enctype="multipart/form-data"
use:enhance={() => {
creating = true;
return async ({ update }) => {
await update({ reset: true });
creating = false;
if (fileInput) fileInput.value = '';
};
}}
>
<h2>Dodaj storitev</h2> <h2>Dodaj storitev</h2>
<label> <label>
Naziv Naziv
@ -22,9 +38,15 @@
Postavke seznama (ena na vrstico, neobvezno) Postavke seznama (ena na vrstico, neobvezno)
<textarea class="admin-input" name="bullets" rows="3"></textarea> <textarea class="admin-input" name="bullets" rows="3"></textarea>
</label> </label>
<label>
Slike (neobvezno, lahko več)
<input bind:this={fileInput} type="file" name="images" accept="image/*" multiple />
</label>
{#if form?.error}<p class="admin-error">{form.error}</p>{/if} {#if form?.error}<p class="admin-error">{form.error}</p>{/if}
{#if form?.success}<p class="admin-msg">{form.success}</p>{/if} {#if form?.success}<p class="admin-msg">{form.success}</p>{/if}
<button class="admin-btn" type="submit">Dodaj</button> <button class="admin-btn" type="submit" disabled={creating}>
{creating ? 'Dodajanje…' : 'Dodaj'}
</button>
</form> </form>
<h2>Obstoječe storitve</h2> <h2>Obstoječe storitve</h2>