fix: restore BottleGrid and apply storage URL normalization
This commit is contained in:
@@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package } from 'lucide-react';
|
import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package } from 'lucide-react';
|
||||||
|
import { getStorageUrl } from '@/lib/supabase';
|
||||||
import TastingNoteForm from '@/components/TastingNoteForm';
|
import TastingNoteForm from '@/components/TastingNoteForm';
|
||||||
import StatusSwitcher from '@/components/StatusSwitcher';
|
import StatusSwitcher from '@/components/StatusSwitcher';
|
||||||
import TastingList from '@/components/TastingList';
|
import TastingList from '@/components/TastingList';
|
||||||
@@ -43,7 +44,7 @@ export default async function BottlePage({ params }: { params: { id: string } })
|
|||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||||||
<div className="aspect-[4/5] rounded-3xl overflow-hidden shadow-2xl border border-zinc-200 dark:border-zinc-800">
|
<div className="aspect-[4/5] rounded-3xl overflow-hidden shadow-2xl border border-zinc-200 dark:border-zinc-800">
|
||||||
<img
|
<img
|
||||||
src={bottle.image_url}
|
src={getStorageUrl(bottle.image_url)}
|
||||||
alt={bottle.name}
|
alt={bottle.name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,10 +2,26 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle } from 'lucide-react';
|
import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle, Trash2, AlertTriangle } from 'lucide-react';
|
||||||
|
import { getStorageUrl } from '@/lib/supabase';
|
||||||
|
|
||||||
|
interface Bottle {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
distillery: string;
|
||||||
|
category: string;
|
||||||
|
abv: number;
|
||||||
|
age: number;
|
||||||
|
image_url: string;
|
||||||
|
status: 'sealed' | 'open' | 'sampled' | 'empty';
|
||||||
|
created_at: string;
|
||||||
|
last_tasted?: string | null;
|
||||||
|
is_whisky?: boolean;
|
||||||
|
confidence?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface BottleCardProps {
|
interface BottleCardProps {
|
||||||
bottle: any;
|
bottle: Bottle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BottleCard({ bottle }: BottleCardProps) {
|
function BottleCard({ bottle }: BottleCardProps) {
|
||||||
@@ -20,13 +36,13 @@ function BottleCard({ bottle }: BottleCardProps) {
|
|||||||
const statusStyle = statusConfig[bottle.status as keyof typeof statusConfig] || statusConfig.sealed;
|
const statusStyle = statusConfig[bottle.status as keyof typeof statusConfig] || statusConfig.sealed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/bottles/${bottle.id}`} className="block h-full group">
|
<Link href={`/ bottles / ${bottle.id} `} className="block h-full group">
|
||||||
<div className="h-full bg-white dark:bg-zinc-900 rounded-[2rem] overflow-hidden border border-zinc-200 dark:border-zinc-800 shadow-sm transition-all duration-300 hover:shadow-2xl hover:shadow-amber-900/10 hover:-translate-y-1 group-hover:border-amber-500/30">
|
<div className="h-full bg-white dark:bg-zinc-900 rounded-[2rem] overflow-hidden border border-zinc-200 dark:border-zinc-800 shadow-sm transition-all duration-300 hover:shadow-2xl hover:shadow-amber-900/10 hover:-translate-y-1 group-hover:border-amber-500/30">
|
||||||
<div className="aspect-[4/3] overflow-hidden bg-zinc-100 dark:bg-zinc-800 relative">
|
<div className="aspect-[4/3] overflow-hidden bg-zinc-100 dark:bg-zinc-800 relative">
|
||||||
<img
|
<img
|
||||||
src={bottle.image_url}
|
src={getStorageUrl(bottle.image_url)}
|
||||||
alt={bottle.name}
|
alt={bottle.name}
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
|
||||||
@@ -37,7 +53,7 @@ function BottleCard({ bottle }: BottleCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`absolute bottom-3 left-3 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase flex items-center gap-2 backdrop-blur-md border shadow-lg ${statusStyle.color}`}>
|
<div className={`absolute bottom - 3 left - 3 px - 3 py - 1.5 rounded - xl text - [10px] font - black uppercase flex items - center gap - 2 backdrop - blur - md border shadow - lg ${statusStyle.color} `}>
|
||||||
<StatusIcon size={12} />
|
<StatusIcon size={12} />
|
||||||
{statusStyle.label}
|
{statusStyle.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -54,8 +70,8 @@ function BottleCard({ bottle }: BottleCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className={`font-black text-xl leading-tight group-hover:text-amber-600 transition-colors line-clamp-2 min-h-[3.5rem] flex items-center ${bottle.is_whisky === false ? 'text-red-600 dark:text-red-400' : 'text-zinc-900 dark:text-zinc-100'
|
<h3 className={`font - black text - xl leading - tight group - hover: text - amber - 600 transition - colors line - clamp - 2 min - h - [3.5rem] flex items - center ${bottle.is_whisky === false ? 'text-red-600 dark:text-red-400' : 'text-zinc-900 dark:text-zinc-100'
|
||||||
}`}>
|
} `}>
|
||||||
{bottle.name || 'Unbekannte Flasche'}
|
{bottle.name || 'Unbekannte Flasche'}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,10 +194,10 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedCategory(null)}
|
onClick={() => setSelectedCategory(null)}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${selectedCategory === null
|
className={`px - 4 py - 2 rounded - xl text - xs font - bold whitespace - nowrap transition - all border ${selectedCategory === null
|
||||||
? 'bg-amber-600 border-amber-600 text-white shadow-lg shadow-amber-600/20'
|
? 'bg-amber-600 border-amber-600 text-white shadow-lg shadow-amber-600/20'
|
||||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
ALLE
|
ALLE
|
||||||
</button>
|
</button>
|
||||||
@@ -189,10 +205,10 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setSelectedCategory(cat)}
|
onClick={() => setSelectedCategory(cat)}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${selectedCategory === cat
|
className={`px - 4 py - 2 rounded - xl text - xs font - bold whitespace - nowrap transition - all border ${selectedCategory === cat
|
||||||
? 'bg-amber-600 border-amber-600 text-white shadow-lg shadow-amber-600/20'
|
? 'bg-amber-600 border-amber-600 text-white shadow-lg shadow-amber-600/20'
|
||||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
{cat.toUpperCase()}
|
{cat.toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
@@ -206,10 +222,10 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedDistillery(null)}
|
onClick={() => setSelectedDistillery(null)}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${selectedDistillery === null
|
className={`px - 4 py - 2 rounded - xl text - xs font - bold whitespace - nowrap transition - all border ${selectedDistillery === null
|
||||||
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
||||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
ALLE
|
ALLE
|
||||||
</button>
|
</button>
|
||||||
@@ -217,10 +233,10 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
<button
|
<button
|
||||||
key={dist}
|
key={dist}
|
||||||
onClick={() => setSelectedDistillery(dist)}
|
onClick={() => setSelectedDistillery(dist)}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${selectedDistillery === dist
|
className={`px - 4 py - 2 rounded - xl text - xs font - bold whitespace - nowrap transition - all border ${selectedDistillery === dist
|
||||||
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
||||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
{dist.toUpperCase()}
|
{dist.toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
@@ -236,10 +252,10 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
|
|||||||
<button
|
<button
|
||||||
key={status}
|
key={status}
|
||||||
onClick={() => setSelectedStatus(selectedStatus === status ? null : status)}
|
onClick={() => setSelectedStatus(selectedStatus === status ? null : status)}
|
||||||
className={`px-4 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${selectedStatus === status
|
className={`px - 4 py - 2 rounded - xl text - xs font - bold whitespace - nowrap transition - all border ${selectedStatus === status
|
||||||
? status === 'open' ? 'bg-amber-500 border-amber-500 text-white' : status === 'sampled' ? 'bg-purple-500 border-purple-500 text-white' : status === 'empty' ? 'bg-zinc-500 border-zinc-500 text-white' : 'bg-blue-600 border-blue-600 text-white'
|
? status === 'open' ? 'bg-amber-500 border-amber-500 text-white' : status === 'sampled' ? 'bg-purple-500 border-purple-500 text-white' : status === 'empty' ? 'bg-zinc-500 border-zinc-500 text-white' : 'bg-blue-600 border-blue-600 text-white'
|
||||||
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
: 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400'
|
||||||
}`}
|
} `}
|
||||||
>
|
>
|
||||||
{status.toUpperCase()}
|
{status.toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,4 +9,32 @@ if (!supabaseUrl || !supabaseAnonKey) {
|
|||||||
|
|
||||||
export const supabase = (supabaseUrl && supabaseAnonKey)
|
export const supabase = (supabaseUrl && supabaseAnonKey)
|
||||||
? createClient(supabaseUrl, supabaseAnonKey)
|
? createClient(supabaseUrl, supabaseAnonKey)
|
||||||
: null as any; // Fallback or handle null in services
|
: null as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert eine Storage-URL oder einen Pfad.
|
||||||
|
* Verhindert "Mixed Content"-Fehler in Self-Hosted Setups,
|
||||||
|
* indem interne IPs durch die öffentliche URL ersetzt werden.
|
||||||
|
*/
|
||||||
|
export function getStorageUrl(path: string | null | undefined): string {
|
||||||
|
if (!path) return '';
|
||||||
|
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '');
|
||||||
|
if (!baseUrl) return path;
|
||||||
|
|
||||||
|
// Wenn es bereits eine URL ist
|
||||||
|
if (path.startsWith('http')) {
|
||||||
|
// Falls die URL eine interne IP enthält (z.B. 192.168.x.x oder localhost)
|
||||||
|
// oder falls sie über http statt https kommt (obwohl die App https nutzt)
|
||||||
|
if (path.includes('192.168.') || path.includes('localhost') || (baseUrl.startsWith('https') && path.startsWith('http:'))) {
|
||||||
|
const storagePathMatch = path.match(/\/storage\/v1\/object\/public\/bottles\/(.+)$/);
|
||||||
|
if (storagePathMatch) {
|
||||||
|
return `${baseUrl}/storage/v1/object/public/bottles/${storagePathMatch[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn es nur ein Pfad ist, vervollständigen
|
||||||
|
return `${baseUrl}/storage/v1/object/public/bottles/${path}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user