diff --git a/app/src/lib/icons.ts b/app/src/lib/icons.ts
index 3d8cb16..bf74341 100644
--- a/app/src/lib/icons.ts
+++ b/app/src/lib/icons.ts
@@ -4,6 +4,7 @@ import calendar from './icons/calendar.svg?raw';
import currencyEur from './icons/currency-eur.svg?raw';
import envelope from './icons/envelope.svg?raw';
import gear from './icons/gear.svg?raw';
+import images from './icons/images.svg?raw';
export const icons = {
house,
@@ -12,6 +13,7 @@ export const icons = {
'currency-eur': currencyEur,
envelope,
gear,
+ images,
} as const;
export type IconName = keyof typeof icons;
diff --git a/app/src/lib/icons/images.svg b/app/src/lib/icons/images.svg
new file mode 100644
index 0000000..48ffc9a
--- /dev/null
+++ b/app/src/lib/icons/images.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts
index bcc34d3..067d0e6 100644
--- a/app/src/lib/types.ts
+++ b/app/src/lib/types.ts
@@ -43,6 +43,24 @@ export interface Mitglied {
}
export type Rolle = 'admin' | 'trainer';
+
+export interface Neuigkeit {
+ id: string;
+ verein_id: string;
+ autor_id: string;
+ text?: string;
+ medien: string[];
+ gruppe_ids: string[];
+ termin_id?: string;
+ created: string;
+ expand?: { autor_id?: { id: string; name: string } };
+}
+
+export interface Reaktion {
+ id: string;
+ beitrag_id: string;
+ user_id: string;
+}
export type Verfuegbarkeit = 'offen' | 'bestaetigt' | 'abgesagt' | 'vertretung_gesucht';
export interface Gruppe {
diff --git a/app/src/routes/(app)/+layout.svelte b/app/src/routes/(app)/+layout.svelte
index 25ea770..fcd5e9c 100644
--- a/app/src/routes/(app)/+layout.svelte
+++ b/app/src/routes/(app)/+layout.svelte
@@ -70,6 +70,7 @@
const isAdmin = () => !pb.authStore.record?.rolle || pb.authStore.record?.rolle === 'admin';
const allNavItems: { href: string; label: string; icon: IconName; adminOnly?: boolean }[] = [
+ { href: '/neuigkeiten', label: 'Neuigkeiten', icon: 'images' },
{ href: '/mitglieder', label: 'Mitglieder', icon: 'users' },
{ href: '/termine', label: 'Termine', icon: 'calendar' },
{ href: '/beitraege', label: 'Beiträge', icon: 'currency-eur', adminOnly: true },
diff --git a/app/src/routes/(app)/neuigkeiten/+page.svelte b/app/src/routes/(app)/neuigkeiten/+page.svelte
new file mode 100644
index 0000000..4591d55
--- /dev/null
+++ b/app/src/routes/(app)/neuigkeiten/+page.svelte
@@ -0,0 +1,479 @@
+
+
+Neuigkeiten — vereins.haus
+
+
+
Neuigkeiten
+ {#if canPost()}
+
+ {/if}
+
+
+{#if loading}
+ Laden…
+{:else if neuigkeiten.length === 0}
+ Noch keine Beiträge – schreib den ersten!
+{:else}
+
+ {#each neuigkeiten as n (n.id)}
+
+
+
{autorName(n)[0]?.toUpperCase()}
+
+ {autorName(n)}
+ {zeitAgo(n.created)}
+
+ {#if n.autor_id === userId()}
+
+ {/if}
+
+
+ {#if n.termin_id}
+ 📅 {terminName(n.termin_id)}
+ {/if}
+
+ {#if n.text}
+ {n.text}
+ {/if}
+
+ {#if n.medien?.length > 0}
+
+ {#each n.medien as datei (datei)}
+ {#if isVideo(datei)}
+
+ {:else}
+
+ {/if}
+ {/each}
+
+ {/if}
+
+
+
+ {#if n.gruppe_ids?.length > 0}
+
+ {n.gruppe_ids.length === 1
+ ? (gruppen.find(g => g.id === n.gruppe_ids[0])?.name ?? '')
+ : `${n.gruppe_ids.length} Gruppen`}
+
+ {/if}
+
+
+ {/each}
+
+{/if}
+
+
+{#if showForm}
+
+{/if}
+
+
+{#if lightboxUrl}
+ lightboxUrl = ''}
+ onkeydown={(e) => e.key === 'Escape' && (lightboxUrl = '')}>
+

+
+{/if}
+
+
diff --git a/pocketbase/pb_migrations/1779230900_neuigkeiten.js b/pocketbase/pb_migrations/1779230900_neuigkeiten.js
new file mode 100644
index 0000000..752dea6
--- /dev/null
+++ b/pocketbase/pb_migrations/1779230900_neuigkeiten.js
@@ -0,0 +1,98 @@
+///
+migrate((app) => {
+
+ // neuigkeiten – Vereins-Feed mit Medien
+ {
+ const c = new Collection({
+ "createRule": "@request.auth.verein_id = verein_id",
+ "deleteRule": "@request.auth.verein_id = verein_id && autor_id = @request.auth.id",
+ "listRule": "@request.auth.verein_id = verein_id",
+ "viewRule": "@request.auth.verein_id = verein_id",
+ "updateRule": "@request.auth.verein_id = verein_id && autor_id = @request.auth.id",
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256",
+ "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$",
+ "primaryKey": true, "required": true, "system": true, "type": "text",
+ "help": "", "hidden": false, "presentable": false
+ },
+ {
+ "type": "relation", "id": "relation2001000200", "name": "verein_id",
+ "help": "", "hidden": false, "presentable": false, "required": true, "system": false,
+ "cascadeDelete": true, "collectionId": "pbc_3589557411", "maxSelect": 1, "minSelect": 0
+ },
+ {
+ "type": "relation", "id": "relation2001000201", "name": "autor_id",
+ "help": "", "hidden": false, "presentable": false, "required": true, "system": false,
+ "cascadeDelete": false, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0
+ },
+ {
+ "type": "text", "id": "text2001000202", "name": "text",
+ "help": "", "hidden": false, "presentable": false, "required": false, "system": false,
+ "autogeneratePattern": "", "min": 0, "max": 0, "pattern": ""
+ },
+ {
+ "type": "file", "id": "file2001000203", "name": "medien",
+ "help": "", "hidden": false, "presentable": false, "required": false, "system": false,
+ "maxSelect": 10, "maxSize": 15728640,
+ "mimeTypes": ["image/jpeg","image/png","image/gif","image/webp","video/mp4","video/quicktime"]
+ },
+ {
+ "type": "relation", "id": "relation2001000204", "name": "gruppe_ids",
+ "help": "", "hidden": false, "presentable": false, "required": false, "system": false,
+ "cascadeDelete": false, "collectionId": "pbc_3099069179", "maxSelect": 99, "minSelect": 0
+ },
+ {
+ "type": "relation", "id": "relation2001000205", "name": "termin_id",
+ "help": "", "hidden": false, "presentable": false, "required": false, "system": false,
+ "cascadeDelete": false, "collectionId": "pbc_2279568741", "maxSelect": 1, "minSelect": 0
+ }
+ ],
+ "id": "pbc_neuigkeiten",
+ "indexes": [],
+ "name": "neuigkeiten",
+ "system": false,
+ "type": "base"
+ })
+ app.save(c)
+ }
+
+ // reaktionen – 👍 pro User pro Beitrag
+ {
+ const c = new Collection({
+ "createRule": "@request.auth.id != ''",
+ "deleteRule": "@request.auth.id = user_id",
+ "listRule": "@request.auth.verein_id = beitrag_id.verein_id",
+ "viewRule": "@request.auth.verein_id = beitrag_id.verein_id",
+ "updateRule": null,
+ "fields": [
+ {
+ "autogeneratePattern": "[a-z0-9]{15}", "id": "text3208210256",
+ "max": 15, "min": 15, "name": "id", "pattern": "^[a-z0-9]+$",
+ "primaryKey": true, "required": true, "system": true, "type": "text",
+ "help": "", "hidden": false, "presentable": false
+ },
+ {
+ "type": "relation", "id": "relation2001000210", "name": "beitrag_id",
+ "help": "", "hidden": false, "presentable": false, "required": true, "system": false,
+ "cascadeDelete": true, "collectionId": "pbc_neuigkeiten", "maxSelect": 1, "minSelect": 0
+ },
+ {
+ "type": "relation", "id": "relation2001000211", "name": "user_id",
+ "help": "", "hidden": false, "presentable": false, "required": true, "system": false,
+ "cascadeDelete": true, "collectionId": "_pb_users_auth_", "maxSelect": 1, "minSelect": 0
+ }
+ ],
+ "id": "pbc_reaktionen",
+ "indexes": ["CREATE UNIQUE INDEX idx_reaktion_unique ON reaktionen (beitrag_id, user_id)"],
+ "name": "reaktionen",
+ "system": false,
+ "type": "base"
+ })
+ app.save(c)
+ }
+
+}, (app) => {
+ try { app.delete(app.findCollectionByNameOrId("pbc_reaktionen")) } catch(_) {}
+ try { app.delete(app.findCollectionByNameOrId("pbc_neuigkeiten")) } catch(_) {}
+})