diff --git a/src/i18n/de.ts b/src/i18n/de.ts
index d005916..46da511 100644
--- a/src/i18n/de.ts
+++ b/src/i18n/de.ts
@@ -1,6 +1,29 @@
import { TranslationKeys } from './types';
export const de: TranslationKeys = {
+ splits: {
+ joinTitle: 'Sample bestellen',
+ amount: 'Menge',
+ shipping: 'Versand',
+ whisky: 'Whisky',
+ glass: 'Sample-Flasche',
+ total: 'Gesamt',
+ requestSent: 'Anfrage gesendet!',
+ requestSentDesc: 'Der Host wird deine Anfrage prüfen und sich bei dir melden.',
+ loginToParticipate: 'Anmelden zum Teilnehmen',
+ loginToParticipateDesc: 'Um an dieser Flaschenteilung teilzunehmen, musst du angemeldet sein.',
+ publicExplore: 'Aktuelle Flaschenteilungen',
+ waitlist: 'Auf Warteliste setzen',
+ sendRequest: 'Anfrage senden',
+ youAreParticipating: 'Du nimmst teil',
+ byHost: 'Von',
+ shareLink: 'Link teilen',
+ backToStart: 'Zurück zum Start',
+ noSplitsFound: 'Keine aktiven Teilungen gefunden',
+ falscheTeilung: 'Flaschenteilung',
+ clFlasche: 'cl Flasche',
+ jahre: 'Jahre',
+ },
common: {
save: 'Speichern',
cancel: 'Abbrechen',
@@ -37,9 +60,14 @@ export const de: TranslationKeys = {
},
searchPlaceholder: 'Flaschen oder Noten suchen...',
noBottles: 'Keine Flaschen gefunden. Zeit für einen Einkauf! 🥃',
- collection: 'Deine Sammlung',
- reTry: 'Erneut versuchen',
+ collection: 'Kollektion',
+ reTry: 'Nochmal versuchen',
all: 'Alle',
+ tagline: 'Modernes Minimalistisches Tasting-Tool.',
+ bottleCount: 'Flaschen',
+ imprint: 'Impressum',
+ privacy: 'Datenschutz',
+ settings: 'Einstellungen',
},
grid: {
searchPlaceholder: 'Suchen nach Name oder Distille...',
@@ -165,6 +193,84 @@ export const de: TranslationKeys = {
noSessions: 'Noch keine Sessions vorhanden.',
expiryWarning: 'Diese Session läuft bald ab.',
},
+ nav: {
+ home: 'Home',
+ shelf: 'Sammlung',
+ activity: 'Aktivität',
+ search: 'Suchen',
+ profile: 'Profil',
+ },
+ hub: {
+ title: 'Activity Hub',
+ subtitle: 'Live-Events & Splits',
+ tabs: {
+ tastings: 'Tastings',
+ splits: 'Splits',
+ },
+ sections: {
+ startSession: 'Neue Session starten',
+ startSplit: 'Neuen Split starten',
+ activeNow: 'Gerade aktiv',
+ yourSessions: 'Deine Sessions',
+ yourSplits: 'Deine Splits',
+ participating: 'Teilnahmen',
+ },
+ placeholders: {
+ sessionName: 'Session-Name (z.B. Islay Nacht)',
+ noSessions: 'Noch keine Sessions',
+ noSplits: 'Noch keine Splits erstellt',
+ openSplitCreator: 'Split-Creator öffnen',
+ },
+ },
+ tutorial: {
+ skip: 'Überspringen',
+ next: 'Weiter',
+ finish: 'Los geht\'s!',
+ steps: {
+ welcome: {
+ title: 'Willkommen bei DramLog!',
+ desc: 'Dein persönliches Whisky-Tagebuch. Scanne, bewerte und entdecke neue Drams.',
+ },
+ scan: {
+ title: 'Scanne deine Flaschen',
+ desc: 'Fotografiere das Etikett einer Flasche – die KI erkennt automatisch alle Details.',
+ },
+ taste: {
+ title: 'Bewerte deine Drams',
+ desc: 'Füge Tasting-Notizen hinzu und behalte den Überblick über deine Lieblings-Whiskys.',
+ },
+ activity: {
+ title: 'Aktivitätshub',
+ desc: 'Organisiere Tasting-Sessions mit Freunden oder nimm an exklusiven Bottle Splits teil.',
+ },
+ ready: {
+ title: 'Bereit zum Start!',
+ desc: 'Scanne jetzt deine erste Flasche mit dem orangefarbenen Button unten.',
+ },
+ },
+ },
+ settings: {
+ title: 'Einstellungen',
+ language: 'Sprache',
+ cookieSettings: 'Cookie-Einstellungen',
+ cookieDesc: 'Diese App verwendet nur technisch notwendige Cookies für die Authentifizierung und funktionale Cookies für UI-Präferenzen.',
+ cookieNecessary: 'Notwendig: Supabase Auth Cookies',
+ cookieFunctional: 'Funktional: Sprache, UI-Status',
+ privacy: 'Datenschutz',
+ privacyDesc: 'Deine Daten werden sicher auf EU-Servern gespeichert.',
+ privacyLink: 'Datenschutzerklärung lesen',
+ memberSince: 'Mitglied seit',
+ password: {
+ title: 'Passwort ändern',
+ newPassword: 'Neues Passwort',
+ confirmPassword: 'Passwort bestätigen',
+ match: 'Passwörter stimmen überein',
+ mismatch: 'Passwörter stimmen nicht überein',
+ tooShort: 'Passwort muss mindestens 6 Zeichen lang sein',
+ success: 'Passwort erfolgreich geändert!',
+ change: 'Passwort ändern',
+ },
+ },
aroma: {
'Apfel': 'Apfel',
'Grüner Apfel': 'Grüner Apfel',
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 2659ac4..6ff06e5 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -1,6 +1,29 @@
import { TranslationKeys } from './types';
export const en: TranslationKeys = {
+ splits: {
+ joinTitle: 'Order Sample',
+ amount: 'Amount',
+ shipping: 'Shipping',
+ whisky: 'Whisky',
+ glass: 'Sample Bottle',
+ total: 'Total',
+ requestSent: 'Request sent!',
+ requestSentDesc: 'The host will review your request and get back to you.',
+ loginToParticipate: 'Login to participate',
+ loginToParticipateDesc: 'You must be logged in to participate in this bottle split.',
+ publicExplore: 'Active Bottle Splits',
+ waitlist: 'Join Waitlist',
+ sendRequest: 'Send Request',
+ youAreParticipating: 'You are participating',
+ byHost: 'By',
+ shareLink: 'Share Link',
+ backToStart: 'Back to start',
+ noSplitsFound: 'No active splits found',
+ falscheTeilung: 'Bottle Split',
+ clFlasche: 'cl Bottle',
+ jahre: 'Years',
+ },
common: {
save: 'Save',
cancel: 'Cancel',
@@ -37,9 +60,14 @@ export const en: TranslationKeys = {
},
searchPlaceholder: 'Search bottles or notes...',
noBottles: 'No bottles found. Time to go shopping! 🥃',
- collection: 'Your Collection',
- reTry: 'Retry',
+ collection: 'Collection',
+ reTry: 'Try Again',
all: 'All',
+ tagline: 'Modern Minimalist Tasting Tool.',
+ bottleCount: 'Bottles',
+ imprint: 'Imprint',
+ privacy: 'Privacy',
+ settings: 'Settings',
},
grid: {
searchPlaceholder: 'Search by name or distillery...',
@@ -165,6 +193,84 @@ export const en: TranslationKeys = {
noSessions: 'No sessions yet.',
expiryWarning: 'This session will expire soon.',
},
+ nav: {
+ home: 'Home',
+ shelf: 'Shelf',
+ activity: 'Activity',
+ search: 'Search',
+ profile: 'Profile',
+ },
+ hub: {
+ title: 'Activity Hub',
+ subtitle: 'Live Events & Splits',
+ tabs: {
+ tastings: 'Tastings',
+ splits: 'Splits',
+ },
+ sections: {
+ startSession: 'Start New Session',
+ startSplit: 'Start New Split',
+ activeNow: 'Active Right Now',
+ yourSessions: 'Your Sessions',
+ yourSplits: 'Your Splits',
+ participating: 'Participating',
+ },
+ placeholders: {
+ sessionName: 'Session Name (e.g. Islay Night)',
+ noSessions: 'No sessions yet',
+ noSplits: 'No splits created',
+ openSplitCreator: 'Open Split Creator',
+ },
+ },
+ tutorial: {
+ skip: 'Skip',
+ next: 'Next',
+ finish: 'Let\'s go!',
+ steps: {
+ welcome: {
+ title: 'Welcome to DramLog!',
+ desc: 'Your personal whisky diary. Scan, rate and discover new drams.',
+ },
+ scan: {
+ title: 'Scan your bottles',
+ desc: 'Take a photo of a bottle label – AI automatically recognizes all details.',
+ },
+ taste: {
+ title: 'Rate your drams',
+ desc: 'Add tasting notes and keep track of your favorite whiskies.',
+ },
+ activity: {
+ title: 'Activity Hub',
+ desc: 'Organize tasting sessions with friends or join exclusive bottle splits.',
+ },
+ ready: {
+ title: 'Ready to start!',
+ desc: 'Scan your first bottle now using the orange button below.',
+ },
+ },
+ },
+ settings: {
+ title: 'Settings',
+ language: 'Language',
+ cookieSettings: 'Cookie Settings',
+ cookieDesc: 'This app uses only technically necessary cookies for authentication and functional cookies for UI preferences.',
+ cookieNecessary: 'Necessary: Supabase Auth Cookies',
+ cookieFunctional: 'Functional: Language, UI Status',
+ privacy: 'Privacy',
+ privacyDesc: 'Your data is securely stored on EU servers.',
+ privacyLink: 'Read Privacy Policy',
+ memberSince: 'Member since',
+ password: {
+ title: 'Change Password',
+ newPassword: 'New Password',
+ confirmPassword: 'Confirm Password',
+ match: 'Passwords match',
+ mismatch: 'Passwords do not match',
+ tooShort: 'Password must be at least 6 characters long',
+ success: 'Password successfully changed!',
+ change: 'Change Password',
+ },
+ },
aroma: {
'Apfel': 'Apple',
'Grüner Apfel': 'Green Apple',
diff --git a/src/i18n/types.ts b/src/i18n/types.ts
index d4f3bb3..c947962 100644
--- a/src/i18n/types.ts
+++ b/src/i18n/types.ts
@@ -1,4 +1,27 @@
export type TranslationKeys = {
+ splits: {
+ joinTitle: string;
+ amount: string;
+ shipping: string;
+ whisky: string;
+ glass: string;
+ total: string;
+ requestSent: string;
+ requestSentDesc: string;
+ loginToParticipate: string;
+ loginToParticipateDesc: string;
+ publicExplore: string;
+ waitlist: string;
+ sendRequest: string;
+ youAreParticipating: string;
+ byHost: string;
+ shareLink: string;
+ backToStart: string;
+ noSplitsFound: string;
+ falscheTeilung: string;
+ clFlasche: string;
+ jahre: string;
+ };
common: {
save: string;
cancel: string;
@@ -38,6 +61,11 @@ export type TranslationKeys = {
collection: string;
reTry: string;
all: string;
+ tagline: string;
+ bottleCount: string;
+ imprint: string;
+ privacy: string;
+ settings: string;
};
grid: {
searchPlaceholder: string;
@@ -163,5 +191,68 @@ export type TranslationKeys = {
noSessions: string;
expiryWarning: string;
};
+ nav: {
+ home: string;
+ shelf: string;
+ activity: string;
+ search: string;
+ profile: string;
+ };
+ hub: {
+ title: string;
+ subtitle: string;
+ tabs: {
+ tastings: string;
+ splits: string;
+ };
+ sections: {
+ startSession: string;
+ startSplit: string;
+ activeNow: string;
+ yourSessions: string;
+ yourSplits: string;
+ participating: string;
+ };
+ placeholders: {
+ sessionName: string;
+ noSessions: string;
+ noSplits: string;
+ openSplitCreator: string;
+ };
+ };
+ tutorial: {
+ skip: string;
+ next: string;
+ finish: string;
+ steps: {
+ welcome: { title: string; desc: string };
+ scan: { title: string; desc: string };
+ taste: { title: string; desc: string };
+ activity: { title: string; desc: string };
+ ready: { title: string; desc: string };
+ };
+ };
+ settings: {
+ title: string;
+ language: string;
+ cookieSettings: string;
+ cookieDesc: string;
+ cookieNecessary: string;
+ cookieFunctional: string;
+ privacy: string;
+ privacyDesc: string;
+ privacyLink: string;
+ memberSince: string;
+ password: {
+ title: string;
+ newPassword: string;
+ confirmPassword: string;
+ match: string;
+ mismatch: string;
+ tooShort: string;
+ success: string;
+ change: string;
+ };
+ };
aroma: Record;
};
diff --git a/src/services/save-tasting.ts b/src/services/save-tasting.ts
index e7f1dea..d614976 100644
--- a/src/services/save-tasting.ts
+++ b/src/services/save-tasting.ts
@@ -22,7 +22,7 @@ export async function saveTasting(rawData: TastingNoteData) {
}
}
- const { data: tasting, error } = await supabase
+ const { data: tasting, error: insertError } = await supabase
.from('tastings')
.insert({
bottle_id: data.bottle_id,
@@ -39,7 +39,29 @@ export async function saveTasting(rawData: TastingNoteData) {
.select()
.single();
- if (error) throw error;
+ if (insertError) {
+ console.error('[saveTasting] Insert error:', {
+ code: insertError.code,
+ message: insertError.message,
+ details: insertError.details,
+ hint: insertError.hint,
+ data: {
+ bottle_id: data.bottle_id,
+ user_id: user.id,
+ session_id: data.session_id
+ }
+ });
+
+ // Check for RLS violation (42501)
+ if ((insertError as any).code === '42501') {
+ return {
+ success: false,
+ error: 'Keine Berechtigung zum Speichern (RLS). Prüfe ob du Besitzer der Flasche bist oder in einer aktiven Session teilnimmst.',
+ code: 'RLS_VIOLATION'
+ };
+ }
+ throw insertError;
+ }
// Add buddy tags if any
if (data.buddy_ids && data.buddy_ids.length > 0) {
@@ -78,11 +100,11 @@ export async function saveTasting(rawData: TastingNoteData) {
revalidatePath(`/bottles/${data.bottle_id}`);
return { success: true, data: tasting };
- } catch (error) {
+ } catch (error: any) {
console.error('Save Tasting Error:', error);
return {
success: false,
- error: error instanceof Error ? error.message : 'Fehler beim Speichern der Tasting Note',
+ error: error instanceof Error ? error.message : (error?.message || 'Fehler beim Speichern.'),
};
}
}
diff --git a/src/services/split-actions.ts b/src/services/split-actions.ts
index 0f0198f..f9b54a2 100644
--- a/src/services/split-actions.ts
+++ b/src/services/split-actions.ts
@@ -206,6 +206,11 @@ export async function getSplitBySlug(slug: string): Promise<{
const remaining = available - taken - reserved;
const bottle = split.bottles as any;
+ if (!bottle) {
+ console.error(`Split ${slug} has no associated bottle data.`);
+ return { success: false, error: 'Flaschendaten für diesen Split fehlen.' };
+ }
+
// Convert sample sizes from DB format
const sampleSizes = ((split.sample_sizes as any[]) || []).map(s => ({
cl: s.cl,
@@ -228,8 +233,8 @@ export async function getSplitBySlug(slug: string): Promise<{
createdAt: split.created_at,
bottle: {
id: bottle.id,
- name: bottle.name,
- distillery: bottle.distillery,
+ name: bottle.name || 'Unbekannte Flasche',
+ distillery: bottle.distillery || 'Unbekannte Destillerie',
imageUrl: bottle.image_url,
abv: bottle.abv,
age: bottle.age,
@@ -500,8 +505,84 @@ export async function getHostSplits(): Promise<{
}
/**
- * Generate forum export text
+ * Get all splits the current user is participating in
*/
+export async function getParticipatingSplits(): Promise<{
+ success: boolean;
+ splits?: Array<{
+ id: string;
+ slug: string;
+ bottleName: string;
+ bottleImage?: string;
+ totalVolume: number;
+ hostShare: number;
+ participantCount: number;
+ amountCl: number;
+ status: string;
+ isActive: boolean;
+ hostName?: string;
+ }>;
+ error?: string;
+}> {
+ const supabase = await createClient();
+
+ try {
+ const { data: { user } } = await supabase.auth.getUser();
+ if (!user) {
+ return { success: false, error: 'Nicht autorisiert' };
+ }
+
+ const { data: participations, error } = await supabase
+ .from('split_participants')
+ .select(`
+ amount_cl,
+ status,
+ bottle_splits!inner (
+ id,
+ public_slug,
+ total_volume,
+ host_share,
+ is_active,
+ host_id,
+ bottles (name, image_url),
+ profiles:host_id (username)
+ )
+ `)
+ .eq('user_id', user.id)
+ .order('created_at', { ascending: false });
+
+ if (error) {
+ console.error('getParticipatingSplits error:', error);
+ return { success: false, error: 'Fehler beim Laden' };
+ }
+
+ return {
+ success: true,
+ splits: (participations || []).map(p => {
+ const split = p.bottle_splits as any;
+ const bottle = split.bottles as any;
+ const hostProfile = split.profiles as any;
+
+ return {
+ id: split.id,
+ slug: split.public_slug,
+ bottleName: bottle?.name || 'Unbekannt',
+ bottleImage: bottle?.image_url,
+ totalVolume: split.total_volume,
+ hostShare: split.host_share,
+ participantCount: 0, // We could count this but might be overkill for list view
+ amountCl: p.amount_cl,
+ status: p.status,
+ isActive: split.is_active,
+ hostName: hostProfile?.username || 'Host',
+ };
+ }),
+ };
+ } catch (error) {
+ console.error('getParticipatingSplits unexpected error:', error);
+ return { success: false, error: 'Unerwarteter Fehler' };
+ }
+}
export async function generateForumExport(splitId: string): Promise<{
success: boolean;
text?: string;
@@ -603,3 +684,51 @@ export async function closeSplit(splitId: string): Promise<{
return { success: false, error: 'Unerwarteter Fehler' };
}
}
+/**
+ * Get all active splits for public discovery
+ */
+export async function getActiveSplits() {
+ const supabase = await createClient();
+
+ try {
+ const { data: splits, error } = await supabase
+ .from('bottle_splits')
+ .select(`
+ id,
+ public_slug,
+ total_volume,
+ host_share,
+ is_active,
+ bottles (name, image_url, distillery),
+ profiles:host_id (username)
+ `)
+ .eq('is_active', true)
+ .order('created_at', { ascending: false });
+
+ if (error) {
+ console.error('getActiveSplits error:', error);
+ return { success: false, error: 'Fehler beim Laden' };
+ }
+
+ return {
+ success: true,
+ splits: (splits || []).map(s => {
+ const bottle = s.bottles as any;
+ const hostProfile = s.profiles as any;
+ return {
+ id: s.id,
+ slug: s.public_slug,
+ bottleName: bottle?.name || 'Unbekannt',
+ bottleImage: bottle?.image_url,
+ distillery: bottle?.distillery,
+ totalVolume: s.total_volume,
+ hostShare: s.host_share,
+ hostName: hostProfile?.username || 'Host',
+ };
+ }),
+ };
+ } catch (error) {
+ console.error('getActiveSplits unexpected error:', error);
+ return { success: false, error: 'Unerwarteter Fehler' };
+ }
+}