Replace all marketing placeholder content with real MYeasyCMS content
- Logo: Replace generic Makerkit SVG with MYeasyCMS branded logo (grid icon + styled text) - Blog: Replace 3 SaaS placeholder posts with 5 real articles (Vereinsverwaltung, SEPA, Website, DSGVO, Mitglieder-Tipps) - Changelog: Replace 6 generic entries with real feature announcements (Verbandsverwaltung, Fischerei, Dateien, Kurse, Einladungen, i18n) - Documentation: Rewrite all 20 docs from Makerkit references to MYeasyCMS content - FAQ: Replace 6 generic SaaS questions with 10 real MYeasyCMS questions - Navigation: Replace Changelog link with Contact in main nav - Footer: Reorganize into Product/Company/Legal sections - Translations: Update all EN marketing strings to match real Com.BISS content
This commit is contained in:
@@ -1,279 +1,37 @@
|
||||
---
|
||||
title: "Features Overview"
|
||||
description: "Send emails and notifications to your users."
|
||||
title: "E-Mail & Newsletter"
|
||||
description: "Kommunizieren Sie mit Ihren Mitgliedern per E-Mail und Newsletter — direkt aus MYeasyCMS."
|
||||
publishedAt: 2024-04-11
|
||||
order: 0
|
||||
order: 1
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
MYeasyCMS integriert E-Mail-Kommunikation und Newsletter-Versand direkt in die Vereinsverwaltung.
|
||||
|
||||
The application includes email functionality for transactional messages and user notifications.
|
||||
## Newsletter erstellen
|
||||
|
||||
## Email Configuration
|
||||
Erstellen Sie Newsletter mit dem integrierten Editor:
|
||||
|
||||
### Supabase Email (Default)
|
||||
1. **Kampagne anlegen** — Betreff und Absender festlegen
|
||||
2. **Inhalt gestalten** — Text, Bilder und Formatierungen hinzufügen
|
||||
3. **Empfänger wählen** — Alle Mitglieder, einzelne Abteilungen oder manuelle Auswahl
|
||||
4. **Vorschau prüfen** — So sieht der Newsletter im Posteingang aus
|
||||
5. **Versenden** — Sofort oder zu einem geplanten Zeitpunkt
|
||||
|
||||
By default, emails are sent through Supabase:
|
||||
- Authentication emails (sign-up, password reset, magic links)
|
||||
- Email verification
|
||||
- Email change confirmation
|
||||
## Abonnentenverwaltung
|
||||
|
||||
### Custom SMTP
|
||||
- **Anmeldung** — Neue Abonnenten über die Vereinswebsite mit Double-Opt-In
|
||||
- **Abmeldung** — Automatischer Abmeldelink in jeder E-Mail
|
||||
- **Statusverfolgung** — Aktive und inaktive Abonnenten im Überblick
|
||||
|
||||
For transactional emails, configure your own SMTP provider:
|
||||
## Vorlagen
|
||||
|
||||
```bash
|
||||
# .env
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-username
|
||||
SMTP_PASSWORD=your-password
|
||||
SMTP_FROM_EMAIL=noreply@yourdomain.com
|
||||
SMTP_FROM_NAME=Your App Name
|
||||
```
|
||||
Erstellen Sie Vorlagen für wiederkehrende Newsletter — zum Beispiel den monatlichen Vereinsrundbrief oder Einladungen zu Veranstaltungen.
|
||||
|
||||
## Sending Emails
|
||||
## Datenschutz
|
||||
|
||||
### Using the Email Service
|
||||
Der Newsletter-Versand entspricht den DSGVO-Anforderungen:
|
||||
|
||||
```typescript
|
||||
import { sendEmail } from '~/lib/email/send-email';
|
||||
|
||||
await sendEmail({
|
||||
to: 'user@example.com',
|
||||
subject: 'Welcome to Our App',
|
||||
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
|
||||
});
|
||||
```
|
||||
|
||||
### Using Email Templates
|
||||
|
||||
Create reusable email templates:
|
||||
|
||||
```typescript
|
||||
// lib/email/templates/welcome-email.tsx
|
||||
import { EmailTemplate } from '~/lib/email/email-template';
|
||||
|
||||
interface WelcomeEmailProps {
|
||||
name: string;
|
||||
loginUrl: string;
|
||||
}
|
||||
|
||||
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<h1>Welcome, {name}!</h1>
|
||||
<p>We're excited to have you on board.</p>
|
||||
<a href={loginUrl}>Get Started</a>
|
||||
</EmailTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
// Send the email
|
||||
import { render } from '@react-email/render';
|
||||
import { WelcomeEmail } from '~/lib/email/templates/welcome-email';
|
||||
|
||||
const html = render(
|
||||
<WelcomeEmail name="John" loginUrl="https://app.com/login" />
|
||||
);
|
||||
|
||||
await sendEmail({
|
||||
to: 'john@example.com',
|
||||
subject: 'Welcome to Our App',
|
||||
html,
|
||||
});
|
||||
```
|
||||
|
||||
## Email Types
|
||||
|
||||
### Transactional Emails
|
||||
|
||||
Emails triggered by user actions:
|
||||
- Welcome emails
|
||||
- Order confirmations
|
||||
- Password resets
|
||||
- Account notifications
|
||||
- Billing updates
|
||||
|
||||
### Marketing Emails
|
||||
|
||||
Promotional and engagement emails:
|
||||
- Product updates
|
||||
- Feature announcements
|
||||
- Newsletters
|
||||
- Onboarding sequences
|
||||
|
||||
## Email Providers
|
||||
|
||||
### Recommended Providers
|
||||
|
||||
**Resend** - Developer-friendly email API
|
||||
```bash
|
||||
npm install resend
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Resend } from 'resend';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
await resend.emails.send({
|
||||
from: 'noreply@yourdomain.com',
|
||||
to: 'user@example.com',
|
||||
subject: 'Welcome',
|
||||
html: emailHtml,
|
||||
});
|
||||
```
|
||||
|
||||
**SendGrid** - Comprehensive email platform
|
||||
```bash
|
||||
npm install @sendgrid/mail
|
||||
```
|
||||
|
||||
```typescript
|
||||
import sgMail from '@sendgrid/mail';
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
||||
|
||||
await sgMail.send({
|
||||
to: 'user@example.com',
|
||||
from: 'noreply@yourdomain.com',
|
||||
subject: 'Welcome',
|
||||
html: emailHtml,
|
||||
});
|
||||
```
|
||||
|
||||
**Postmark** - Fast transactional email
|
||||
```bash
|
||||
npm install postmark
|
||||
```
|
||||
|
||||
## In-App Notifications
|
||||
|
||||
### Notification System
|
||||
|
||||
Send in-app notifications to users:
|
||||
|
||||
```typescript
|
||||
import { createNotification } from '~/lib/notifications';
|
||||
|
||||
await createNotification({
|
||||
userId: user.id,
|
||||
title: 'New Message',
|
||||
message: 'You have a new message from John',
|
||||
type: 'info',
|
||||
link: '/messages/123',
|
||||
});
|
||||
```
|
||||
|
||||
### Notification Types
|
||||
|
||||
```typescript
|
||||
type NotificationType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
await createNotification({
|
||||
userId: user.id,
|
||||
title: 'Payment Successful',
|
||||
message: 'Your subscription has been renewed',
|
||||
type: 'success',
|
||||
});
|
||||
```
|
||||
|
||||
### Fetching Notifications
|
||||
|
||||
```typescript
|
||||
import { getUserNotifications } from '~/lib/notifications';
|
||||
|
||||
const notifications = await getUserNotifications(userId, {
|
||||
limit: 10,
|
||||
unreadOnly: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Marking as Read
|
||||
|
||||
```typescript
|
||||
import { markNotificationAsRead } from '~/lib/notifications';
|
||||
|
||||
await markNotificationAsRead(notificationId);
|
||||
```
|
||||
|
||||
## Real-time Notifications
|
||||
|
||||
Use Supabase Realtime for instant notifications:
|
||||
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
|
||||
export function NotificationListener() {
|
||||
const supabase = useSupabase();
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel('notifications')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'notifications',
|
||||
filter: `user_id=eq.${userId}`,
|
||||
},
|
||||
(payload) => {
|
||||
// Show toast notification
|
||||
toast.info(payload.new.title);
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Email Templates Best Practices
|
||||
|
||||
1. **Keep it simple** - Plain text and minimal HTML
|
||||
2. **Mobile responsive** - Most emails are read on mobile
|
||||
3. **Clear CTAs** - Make action buttons prominent
|
||||
4. **Personalize** - Use user's name and relevant data
|
||||
5. **Test rendering** - Check across email clients
|
||||
6. **Include plain text** - Always provide text alternative
|
||||
7. **Unsubscribe link** - Required for marketing emails
|
||||
|
||||
## Testing Emails
|
||||
|
||||
### Local Development
|
||||
|
||||
In development, emails are caught by InBucket:
|
||||
|
||||
```
|
||||
http://localhost:54324
|
||||
```
|
||||
|
||||
### Preview Emails
|
||||
|
||||
Use React Email to preview templates:
|
||||
|
||||
```bash
|
||||
npm run email:dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3001` to see email previews.
|
||||
|
||||
## Deliverability Tips
|
||||
|
||||
1. **Authenticate your domain** - Set up SPF, DKIM, DMARC
|
||||
2. **Warm up your domain** - Start with low volumes
|
||||
3. **Monitor bounce rates** - Keep below 5%
|
||||
4. **Avoid spam triggers** - Don't use all caps, excessive punctuation
|
||||
5. **Provide value** - Only send relevant, useful emails
|
||||
6. **Easy unsubscribe** - Make it one-click simple
|
||||
- Double-Opt-In für die Anmeldung
|
||||
- Dokumentation der Einwilligung
|
||||
- Einfache Abmeldemöglichkeit in jeder E-Mail
|
||||
|
||||
@@ -1,16 +1,73 @@
|
||||
---
|
||||
title: "Features"
|
||||
description: "Learn about the built-in features available in MakerKit."
|
||||
title: "Funktionen"
|
||||
description: "Überblick über die Kernfunktionen von MYeasyCMS — Mitgliederverwaltung, Finanzen, Kurse, Veranstaltungen und mehr."
|
||||
publishedAt: 2024-04-11
|
||||
order: 3
|
||||
order: 0
|
||||
status: "published"
|
||||
---
|
||||
|
||||
MakerKit comes with a rich set of features to help you build your SaaS quickly.
|
||||
MYeasyCMS bietet eine umfassende Palette von Funktionen für die tägliche Vereinsarbeit.
|
||||
|
||||
This section covers:
|
||||
- Team collaboration
|
||||
- File uploads
|
||||
- Email functionality
|
||||
- User management
|
||||
- And more built-in features
|
||||
## Mitgliederverwaltung
|
||||
|
||||
- **Stammdaten** — Name, Adresse, Kontaktdaten, Geburtsdatum, Fotos
|
||||
- **Abteilungen** — Mitglieder verschiedenen Abteilungen zuordnen
|
||||
- **SEPA-Mandate** — Lastschriftmandate direkt im Mitgliedsstamm verwalten
|
||||
- **Mitgliedsausweise** — Ausweise aus Vorlagen generieren (mit Foto und Barcode)
|
||||
- **Ein-/Austritte** — Historisierte Dokumentation aller Statusänderungen
|
||||
- **Import/Export** — Mitgliederlisten aus Excel/CSV importieren und exportieren
|
||||
- **Statistiken** — Mitgliederentwicklung, Altersstruktur, Abteilungsverteilung
|
||||
|
||||
## Finanzen & SEPA
|
||||
|
||||
- **Beitragskategorien** — Verschiedene Beitragssätze definieren und zuordnen
|
||||
- **SEPA-Sammellastschrift** — XML-Dateien (pain.008) für den Bankeinzug erzeugen
|
||||
- **Rechnungen** — Erstellen, versenden und nachverfolgen
|
||||
- **Zahlungsverfolgung** — Offene Posten, Mahnwesen, Rückläufer
|
||||
- **Geschäftsjahre** — Jahresabschluss und Übertrag
|
||||
|
||||
## Kursverwaltung
|
||||
|
||||
- **Kurse anlegen** — Titel, Beschreibung, Termine, Dozent, Kosten
|
||||
- **Teilnehmerverwaltung** — Anmeldungen, Wartelisten, Anwesenheit
|
||||
- **Kalender** — Übersichtliche Terminplanung mit Ferien- und Feiertagsberücksichtigung
|
||||
- **Kategorien und Orte** — Kurstypen und Veranstaltungsorte verwalten
|
||||
- **Online-Anmeldung** — Teilnehmer können sich über die Vereinswebsite anmelden
|
||||
|
||||
## Veranstaltungen
|
||||
|
||||
- **Eventplanung** — Veranstaltungen mit Beschreibung, Ort, Datum und Kapazität
|
||||
- **Registrierungen** — Online-Anmeldung und Teilnehmerlisten
|
||||
- **Ferienpässe** — Verwaltung von Ferienprogrammen (entwickelt mit der Stadt Regensburg)
|
||||
|
||||
## Newsletter
|
||||
|
||||
- **Kampagnen erstellen** — E-Mails mit Vorlagen gestalten
|
||||
- **Empfänger verwalten** — Versand an Abteilungen, Gruppen oder individuelle Listen
|
||||
- **An-/Abmeldung** — Automatische Verwaltung mit Double-Opt-In
|
||||
|
||||
## Dokumente
|
||||
|
||||
- **Vorlagen** — Briefvorlagen, Etiketten, Ausweise, Zertifikate
|
||||
- **Seriendokumente** — Massengenerierung für alle Mitglieder oder Gruppen
|
||||
- **Export** — PDF- und Excel-Export
|
||||
|
||||
## Site-Builder
|
||||
|
||||
- **Drag-and-Drop** — Seiten visuell zusammenbauen
|
||||
- **Vereinsdaten einbinden** — Kurse, Veranstaltungen und Kontakte direkt anzeigen
|
||||
- **Responsive** — Automatisch für alle Bildschirmgrößen optimiert
|
||||
|
||||
## Fischereiverwaltung
|
||||
|
||||
- **Gewässer** — Gewässerverzeichnis mit Details und Karten
|
||||
- **Besatzmaßnahmen** — Dokumentation von Fischbesatz
|
||||
- **Fangbücher** — Digitale Fangbuchführung
|
||||
- **Erlaubnisscheine** — Ausgabe und Verwaltung von Fischereierlaubnisscheinen
|
||||
- **Statistiken** — Fang- und Besatzauswertungen
|
||||
|
||||
## Sitzungsprotokolle
|
||||
|
||||
- **Protokolle** — Sitzungsprotokolle mit Tagesordnungspunkten erstellen
|
||||
- **Aufgaben** — Aufgaben aus Protokollen ableiten und nachverfolgen
|
||||
- **Beschlüsse** — Beschlüsse dokumentieren und archivieren
|
||||
|
||||
@@ -1,398 +1,37 @@
|
||||
---
|
||||
title: "File Uploads"
|
||||
description: "Handle file uploads with Supabase Storage."
|
||||
title: "Dateien und Dokumente"
|
||||
description: "Verwalten und generieren Sie Dokumente — Mitgliedsausweise, Rechnungen, Briefe und mehr."
|
||||
publishedAt: 2024-04-11
|
||||
order: 2
|
||||
order: 3
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
MYeasyCMS bietet integrierte Dokumentenverwaltung und -generierung.
|
||||
|
||||
Enable users to upload and manage files using Supabase Storage.
|
||||
## Dokumentenvorlagen
|
||||
|
||||
## Setup
|
||||
Erstellen Sie Vorlagen für häufig benötigte Dokumente:
|
||||
|
||||
### Create Storage Bucket
|
||||
- **Mitgliedsausweise** — Mit Foto, Mitgliedsnummer und Barcode
|
||||
- **Rechnungen** — Für Beiträge, Kursgebühren und sonstige Leistungen
|
||||
- **Briefe** — Serienbriefe an Mitglieder oder Gruppen
|
||||
- **Etiketten** — Adressetiketten für den Postversand
|
||||
- **Zertifikate** — Teilnahmebestätigungen für Kurse und Lehrgänge
|
||||
- **Berichte** — Mitgliederlisten und Statistiken
|
||||
|
||||
```sql
|
||||
-- Create a public bucket for avatars
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('avatars', 'avatars', true);
|
||||
## Seriendokumente
|
||||
|
||||
-- Create a private bucket for documents
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('documents', 'documents', false);
|
||||
```
|
||||
Generieren Sie Dokumente für mehrere Mitglieder gleichzeitig. Platzhalter in der Vorlage werden automatisch mit den Mitgliederdaten gefüllt.
|
||||
|
||||
### Set Storage Policies
|
||||
## Dateiverwaltung
|
||||
|
||||
```sql
|
||||
-- Allow users to upload their own avatars
|
||||
CREATE POLICY "Users can upload their own avatar"
|
||||
ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'avatars' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
Laden Sie Dateien direkt in MYeasyCMS hoch und organisieren Sie Ihre Vereinsdokumente:
|
||||
|
||||
-- Allow users to view their own avatars
|
||||
CREATE POLICY "Users can view their own avatar"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (
|
||||
bucket_id = 'avatars' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
- Satzung und Geschäftsordnung
|
||||
- Protokolle und Beschlüsse
|
||||
- Formulare und Vordrucke
|
||||
- Fotos und Medien
|
||||
|
||||
-- Allow users to delete their own avatars
|
||||
CREATE POLICY "Users can delete their own avatar"
|
||||
ON storage.objects FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'avatars' AND
|
||||
auth.uid()::text = (storage.foldername(name))[1]
|
||||
);
|
||||
```
|
||||
## Export
|
||||
|
||||
## Upload Component
|
||||
|
||||
### Basic File Upload
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { uploadFileAction } from '../_lib/actions';
|
||||
|
||||
export function FileUpload() {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const result = await uploadFileAction(formData);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('File uploaded successfully');
|
||||
}
|
||||
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => setFile(e.files?.[0] || null)}
|
||||
accept="image/*"
|
||||
/>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={!file || uploading}
|
||||
>
|
||||
{uploading ? 'Uploading...' : 'Upload'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
export const uploadFileAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
throw new Error('No file provided');
|
||||
}
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${user.id}/${Date.now()}.${fileExt}`;
|
||||
|
||||
const { data, error } = await client.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Get public URL
|
||||
const { data: { publicUrl } } = client.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
path: data.path,
|
||||
};
|
||||
},
|
||||
{ auth: true }
|
||||
);
|
||||
```
|
||||
|
||||
## Drag and Drop Upload
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
export function DragDropUpload() {
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
for (const file of acceptedFiles) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await uploadFileAction(formData);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
|
||||
},
|
||||
maxSize: 5 * 1024 * 1024, // 5MB
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
'border-2 border-dashed rounded-lg p-8 text-center cursor-pointer',
|
||||
isDragActive && 'border-primary bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<p>Drop files here...</p>
|
||||
) : (
|
||||
<p>Drag and drop files here, or click to select</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## File Validation
|
||||
|
||||
### Client-Side Validation
|
||||
|
||||
```typescript
|
||||
function validateFile(file: File) {
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File size must be less than 5MB');
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('File type must be JPEG, PNG, or GIF');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### Server-Side Validation
|
||||
|
||||
```typescript
|
||||
export const uploadFileAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
// Validate file size
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
throw new Error('File too large');
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type');
|
||||
}
|
||||
|
||||
// Validate dimensions for images
|
||||
if (file.type.startsWith('image/')) {
|
||||
const dimensions = await getImageDimensions(file);
|
||||
if (dimensions.width > 4000 || dimensions.height > 4000) {
|
||||
throw new Error('Image dimensions too large');
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with upload...
|
||||
},
|
||||
{ auth: true }
|
||||
);
|
||||
```
|
||||
|
||||
## Image Optimization
|
||||
|
||||
### Resize on Upload
|
||||
|
||||
```typescript
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const uploadAvatarAction = enhanceAction(
|
||||
async (formData: FormData, user) => {
|
||||
const file = formData.get('file') as File;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Resize image
|
||||
const resized = await sharp(buffer)
|
||||
.resize(200, 200, {
|
||||
fit: 'cover',
|
||||
position: 'center',
|
||||
})
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
const fileName = `${user.id}/avatar.jpg`;
|
||||
|
||||
const { error } = await client.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, resized, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{ auth: true }
|
||||
);
|
||||
```
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function UploadWithProgress() {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
const client = getSupabaseBrowserClient();
|
||||
|
||||
const { error } = await client.storage
|
||||
.from('documents')
|
||||
.upload(`uploads/${file.name}`, file, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const percent = (progressEvent.loaded / progressEvent.total) * 100;
|
||||
setProgress(Math.round(percent));
|
||||
},
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="file" onChange={(e) => handleUpload(e.target.files![0])} />
|
||||
{progress > 0 && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Downloading Files
|
||||
|
||||
### Get Public URL
|
||||
|
||||
```typescript
|
||||
const { data } = client.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl('user-id/avatar.jpg');
|
||||
|
||||
console.log(data.publicUrl);
|
||||
```
|
||||
|
||||
### Download Private File
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client.storage
|
||||
.from('documents')
|
||||
.download('private-file.pdf');
|
||||
|
||||
if (data) {
|
||||
const url = URL.createObjectURL(data);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'file.pdf';
|
||||
a.click();
|
||||
}
|
||||
```
|
||||
|
||||
### Generate Signed URL
|
||||
|
||||
```typescript
|
||||
const { data, error } = await client.storage
|
||||
.from('documents')
|
||||
.createSignedUrl('private-file.pdf', 3600); // 1 hour
|
||||
|
||||
console.log(data.signedUrl);
|
||||
```
|
||||
|
||||
## Deleting Files
|
||||
|
||||
```typescript
|
||||
export const deleteFileAction = enhanceAction(
|
||||
async (data, user) => {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { error } = await client.storage
|
||||
.from('avatars')
|
||||
.remove([data.path]);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
auth: true,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Validate on both sides** - Client and server
|
||||
2. **Limit file sizes** - Prevent abuse
|
||||
3. **Sanitize filenames** - Remove special characters
|
||||
4. **Use unique names** - Prevent collisions
|
||||
5. **Optimize images** - Resize before upload
|
||||
6. **Set storage policies** - Control access
|
||||
7. **Monitor usage** - Track storage costs
|
||||
8. **Clean up unused files** - Regular maintenance
|
||||
9. **Use CDN** - For public files
|
||||
10. **Implement virus scanning** - For user uploads
|
||||
Alle generierten Dokumente können als PDF heruntergeladen werden. Listen und Auswertungen sind zusätzlich als Excel-Export verfügbar.
|
||||
|
||||
@@ -1,276 +1,32 @@
|
||||
---
|
||||
title: "Team Collaboration"
|
||||
description: "Manage team members, roles, and permissions in your application."
|
||||
title: "Teamarbeit"
|
||||
description: "Arbeiten Sie im Team — mit Rollen, Berechtigungen und gemeinsamer Datenpflege."
|
||||
publishedAt: 2024-04-11
|
||||
order: 1
|
||||
order: 2
|
||||
status: "published"
|
||||
---
|
||||
|
||||
> **Note:** This is mock/placeholder content for demonstration purposes.
|
||||
MYeasyCMS ist für die Zusammenarbeit im Vereinsvorstand konzipiert. Mehrere Benutzer können gleichzeitig arbeiten.
|
||||
|
||||
Enable teams to collaborate effectively with built-in team management features.
|
||||
## Mehrere Benutzer
|
||||
|
||||
## Team Accounts
|
||||
Fügen Sie beliebig viele Benutzer zu Ihrem Vereins-Account hinzu. Es gibt keine Begrenzung der Benutzeranzahl — alle Tarife enthalten unbegrenzte Zugänge.
|
||||
|
||||
The application supports multi-tenant team accounts where multiple users can collaborate.
|
||||
## Rollenbasierte Zugriffskontrolle
|
||||
|
||||
### Creating a Team
|
||||
Jeder Benutzer sieht nur die Daten, die für seine Aufgabe relevant sind:
|
||||
|
||||
Users can create new team accounts:
|
||||
| Rolle | Zugriff |
|
||||
|-------|---------|
|
||||
| Administrator | Alle Module und Einstellungen |
|
||||
| Kassenwart | Finanzen, Beiträge, Mitgliederdaten |
|
||||
| Kursleiter | Kursverwaltung, Teilnehmer |
|
||||
| Schriftführer | Protokolle, Dokumente |
|
||||
|
||||
```typescript
|
||||
import { createTeamAccount } from '~/lib/teams/create-team';
|
||||
## Einladungen
|
||||
|
||||
const team = await createTeamAccount({
|
||||
name: 'Acme Corp',
|
||||
slug: 'acme-corp',
|
||||
ownerId: currentUser.id,
|
||||
});
|
||||
```
|
||||
Neue Benutzer werden per E-Mail eingeladen. Die Einladung enthält einen Registrierungslink, über den der neue Benutzer sein Konto erstellt.
|
||||
|
||||
### Team Workspace
|
||||
## Aktivitätsprotokoll
|
||||
|
||||
Each team has its own workspace with isolated data:
|
||||
- Projects and resources
|
||||
- Team-specific settings
|
||||
- Billing and subscription
|
||||
- Activity logs
|
||||
|
||||
## Inviting Members
|
||||
|
||||
### Send Invitations
|
||||
|
||||
Invite new members to your team:
|
||||
|
||||
```typescript
|
||||
import { inviteTeamMember } from '~/lib/teams/invitations';
|
||||
|
||||
await inviteTeamMember({
|
||||
teamId: team.id,
|
||||
email: 'member@example.com',
|
||||
role: 'member',
|
||||
});
|
||||
```
|
||||
|
||||
### Invitation Flow
|
||||
|
||||
1. Owner sends invitation via email
|
||||
2. Recipient receives email with invitation link
|
||||
3. Recipient accepts invitation
|
||||
4. Member gains access to team workspace
|
||||
|
||||
### Managing Invitations
|
||||
|
||||
```tsx
|
||||
import { PendingInvitations } from '~/components/teams/pending-invitations';
|
||||
|
||||
<PendingInvitations teamId={team.id} />
|
||||
```
|
||||
|
||||
## Roles and Permissions
|
||||
|
||||
### Default Roles
|
||||
|
||||
**Owner**
|
||||
- Full access to team and settings
|
||||
- Manage billing and subscriptions
|
||||
- Invite and remove members
|
||||
- Delete team
|
||||
|
||||
**Admin**
|
||||
- Manage team members
|
||||
- Manage team resources
|
||||
- Cannot access billing
|
||||
- Cannot delete team
|
||||
|
||||
**Member**
|
||||
- View team resources
|
||||
- Create and edit own content
|
||||
- Limited team settings access
|
||||
|
||||
### Custom Roles
|
||||
|
||||
Define custom roles with specific permissions:
|
||||
|
||||
```typescript
|
||||
const customRole = {
|
||||
name: 'Editor',
|
||||
permissions: [
|
||||
'read:projects',
|
||||
'write:projects',
|
||||
'read:members',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Checking Permissions
|
||||
|
||||
```typescript
|
||||
import { checkPermission } from '~/lib/teams/permissions';
|
||||
|
||||
const canEdit = await checkPermission(userId, teamId, 'write:projects');
|
||||
|
||||
if (!canEdit) {
|
||||
throw new Error('Insufficient permissions');
|
||||
}
|
||||
```
|
||||
|
||||
## Member Management
|
||||
|
||||
### Listing Members
|
||||
|
||||
```typescript
|
||||
import { getTeamMembers } from '~/lib/teams/members';
|
||||
|
||||
const members = await getTeamMembers(teamId);
|
||||
```
|
||||
|
||||
### Updating Member Role
|
||||
|
||||
```typescript
|
||||
import { updateMemberRole } from '~/lib/teams/members';
|
||||
|
||||
await updateMemberRole({
|
||||
memberId: member.id,
|
||||
role: 'admin',
|
||||
});
|
||||
```
|
||||
|
||||
### Removing Members
|
||||
|
||||
```typescript
|
||||
import { removeMember } from '~/lib/teams/members';
|
||||
|
||||
await removeMember(memberId);
|
||||
```
|
||||
|
||||
## Team Settings
|
||||
|
||||
### Updating Team Info
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { updateTeamAction } from '../_lib/server/actions';
|
||||
|
||||
export function TeamSettingsForm({ team }) {
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: team.name,
|
||||
description: team.description,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
await updateTeamAction({ teamId: team.id, ...data });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input {...register('name')} placeholder="Team name" />
|
||||
<textarea {...register('description')} placeholder="Description" />
|
||||
<button type="submit">Save Changes</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Team Avatar
|
||||
|
||||
```typescript
|
||||
import { uploadTeamAvatar } from '~/lib/teams/avatar';
|
||||
|
||||
const avatarUrl = await uploadTeamAvatar({
|
||||
teamId: team.id,
|
||||
file: avatarFile,
|
||||
});
|
||||
```
|
||||
|
||||
## Activity Log
|
||||
|
||||
Track team activity for transparency:
|
||||
|
||||
```typescript
|
||||
import { logActivity } from '~/lib/teams/activity';
|
||||
|
||||
await logActivity({
|
||||
teamId: team.id,
|
||||
userId: user.id,
|
||||
action: 'member_invited',
|
||||
metadata: {
|
||||
invitedEmail: 'new@example.com',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Viewing Activity
|
||||
|
||||
```typescript
|
||||
import { getTeamActivity } from '~/lib/teams/activity';
|
||||
|
||||
const activities = await getTeamActivity(teamId, {
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
```
|
||||
|
||||
## Team Switching
|
||||
|
||||
Allow users to switch between their teams:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||
|
||||
export function TeamSwitcher() {
|
||||
const { accounts, account } = useTeamAccountWorkspace();
|
||||
|
||||
return (
|
||||
<select
|
||||
value={account.id}
|
||||
onChange={(e) => switchTeam(e.target.value)}
|
||||
>
|
||||
{accounts.map((team) => (
|
||||
<option key={team.id} value={team.id}>
|
||||
{team.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
### Member Joined
|
||||
|
||||
```typescript
|
||||
await createNotification({
|
||||
teamId: team.id,
|
||||
title: 'New Member',
|
||||
message: `${user.name} joined the team`,
|
||||
type: 'info',
|
||||
});
|
||||
```
|
||||
|
||||
### Role Changed
|
||||
|
||||
```typescript
|
||||
await createNotification({
|
||||
userId: member.userId,
|
||||
title: 'Role Updated',
|
||||
message: `Your role was changed to ${newRole}`,
|
||||
type: 'info',
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Clear role hierarchy** - Define roles that make sense for your use case
|
||||
2. **Principle of least privilege** - Give minimum required permissions
|
||||
3. **Audit trail** - Log important team actions
|
||||
4. **Easy onboarding** - Simple invitation process
|
||||
5. **Self-service** - Let members manage their own settings
|
||||
6. **Transparent billing** - Show usage and costs clearly
|
||||
Unter **Verwaltung → Audit** sehen Administratoren, wer wann welche Änderungen vorgenommen hat. Das Protokoll hilft bei der Nachverfolgung und Qualitätssicherung.
|
||||
|
||||
Reference in New Issue
Block a user