feat: Upgrade to Next.js 16.1 & React 19.2, migrate to Supabase SSR with async client handling
This commit is contained in:
3
next-env.d.ts
vendored
3
next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -12,34 +12,34 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"@supabase/auth-helpers-nextjs": "^0.10.0",
|
"@supabase/ssr": "^0.5.2",
|
||||||
"@supabase/supabase-js": "^2.39.0",
|
"@supabase/supabase-js": "^2.47.10",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
"canvas-confetti": "^1.9.2",
|
"canvas-confetti": "^1.9.3",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"dexie-react-hooks": "^4.2.0",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"lucide-react": "^0.300.0",
|
"lucide-react": "^0.468.0",
|
||||||
"next": "14.2.23",
|
"next": "16.1.0",
|
||||||
"openai": "^6.15.0",
|
"openai": "^6.15.0",
|
||||||
"react": "^18",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^19.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.1",
|
"@testing-library/react": "^16.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.0.1",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.23",
|
"eslint-config-next": "16.1.0",
|
||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.3.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^3.3.0",
|
||||||
|
|||||||
750
pnpm-lock.yaml
generated
750
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,12 @@
|
|||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { checkIsAdmin, getGlobalApiStats } from '@/services/track-api-usage';
|
import { checkIsAdmin, getGlobalApiStats } from '@/services/track-api-usage';
|
||||||
import { BarChart3, TrendingUp, Users, Calendar, AlertCircle } from 'lucide-react';
|
import { BarChart3, TrendingUp, Users, Calendar, AlertCircle } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
console.log('[Admin Page] User:', user?.id, user?.email);
|
console.log('[Admin Page] User:', user?.id, user?.email);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||||
import { getAllPlans } from '@/services/subscription-service';
|
import { getAllPlans } from '@/services/subscription-service';
|
||||||
@@ -9,7 +8,7 @@ import { ChevronLeft, Package } from 'lucide-react';
|
|||||||
import PlanManagementClient from '@/components/PlanManagementClient';
|
import PlanManagementClient from '@/components/PlanManagementClient';
|
||||||
|
|
||||||
export default async function AdminPlansPage() {
|
export default async function AdminPlansPage() {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, Tag as TagIcon, Plus, Search, Trash2, Shield, User, Filter, Download } from 'lucide-react';
|
import { ChevronLeft, Tag as TagIcon, Plus, Search, Trash2, Shield, User, Filter, Download } from 'lucide-react';
|
||||||
import { Tag, TagCategory, getTagsByCategory } from '@/services/tags';
|
import { Tag, TagCategory, getTagsByCategory } from '@/services/tags';
|
||||||
@@ -9,7 +9,7 @@ import { useI18n } from '@/i18n/I18nContext';
|
|||||||
|
|
||||||
export default function AdminTagsPage() {
|
export default function AdminTagsPage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import { checkIsAdmin } from '@/services/track-api-usage';
|
import { checkIsAdmin } from '@/services/track-api-usage';
|
||||||
import { getAllUsersWithCredits } from '@/services/admin-credit-service';
|
import { getAllUsersWithCredits } from '@/services/admin-credit-service';
|
||||||
@@ -10,7 +9,7 @@ import { ChevronLeft, Users, Coins, TrendingUp, TrendingDown } from 'lucide-reac
|
|||||||
import UserManagementClient from '@/components/UserManagementClient';
|
import UserManagementClient from '@/components/UserManagementClient';
|
||||||
|
|
||||||
export default async function AdminUsersPage() {
|
export default async function AdminUsersPage() {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Get current user
|
// Get current user
|
||||||
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
export const dynamic = 'force-dynamic';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
const supabase = createRouteHandlerClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check session
|
// Check session
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
export const dynamic = 'force-dynamic';
|
||||||
import { cookies } from 'next/headers';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
@@ -7,7 +7,7 @@ export async function GET(request: Request) {
|
|||||||
const code = requestUrl.searchParams.get('code');
|
const code = requestUrl.searchParams.get('code');
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
const supabase = createRouteHandlerClient({ cookies });
|
const supabase = await createClient();
|
||||||
await supabase.auth.exchangeCodeForSession(code);
|
await supabase.auth.exchangeCodeForSession(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
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, PlusCircle, Info } from 'lucide-react';
|
import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, PlusCircle, Info } from 'lucide-react';
|
||||||
@@ -10,13 +9,12 @@ import DeleteBottleButton from '@/components/DeleteBottleButton';
|
|||||||
import EditBottleForm from '@/components/EditBottleForm';
|
import EditBottleForm from '@/components/EditBottleForm';
|
||||||
import { validateSession } from '@/services/validate-session';
|
import { validateSession } from '@/services/validate-session';
|
||||||
|
|
||||||
export default async function BottlePage({
|
export default async function BottlePage(props: {
|
||||||
params,
|
params: Promise<{ id: string }>,
|
||||||
searchParams
|
searchParams: Promise<{ session_id?: string }>
|
||||||
}: {
|
|
||||||
params: { id: string },
|
|
||||||
searchParams: { session_id?: string }
|
|
||||||
}) {
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
let sessionId = searchParams.session_id;
|
let sessionId = searchParams.session_id;
|
||||||
|
|
||||||
// Validate Session Age (12 hour limit)
|
// Validate Session Age (12 hour limit)
|
||||||
@@ -26,7 +24,7 @@ export default async function BottlePage({
|
|||||||
sessionId = undefined;
|
sessionId = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
const { data: bottle } = await supabase
|
const { data: bottle } = await supabase
|
||||||
|
|||||||
36
src/app/global-error.tsx
Normal file
36
src/app/global-error.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { RefreshCcw } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function GlobalError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="de">
|
||||||
|
<body>
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-50 dark:bg-black text-center">
|
||||||
|
<div className="bg-white dark:bg-zinc-900 p-8 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-xl max-w-md w-full space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-2xl font-black text-zinc-900 dark:text-white">Kritischer Fehler</h2>
|
||||||
|
<p className="text-zinc-500 text-sm">
|
||||||
|
Ein schwerwiegender Fehler ist aufgetreten. Bitte versuche die Seite neu zu laden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => reset()}
|
||||||
|
className="w-full py-4 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 transition-all"
|
||||||
|
>
|
||||||
|
<RefreshCcw size={18} />
|
||||||
|
Erneut versuchen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/app/loading.tsx
Normal file
12
src/app/loading.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-50 dark:bg-black text-center">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Loader2 size={40} className="animate-spin text-amber-600" />
|
||||||
|
<p className="text-zinc-500 font-medium animate-pulse">Whisky Vault wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/app/not-found.tsx
Normal file
29
src/app/not-found.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { Home, MoveLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-zinc-50 dark:bg-black text-center">
|
||||||
|
<div className="bg-white dark:bg-zinc-900 p-8 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-xl max-w-md w-full space-y-6">
|
||||||
|
<div className="text-8xl font-black text-amber-600/20">404</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h2 className="text-2xl font-black text-zinc-900 dark:text-white">Seite nicht gefunden</h2>
|
||||||
|
<p className="text-zinc-500 text-sm">
|
||||||
|
Die gesuchte Seite existiert leider nicht oder wurde verschoben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="w-full py-4 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 transition-all shadow-lg shadow-amber-600/20"
|
||||||
|
>
|
||||||
|
<Home size={18} />
|
||||||
|
Zurück zum Vault
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import CameraCapture from "@/components/CameraCapture";
|
import CameraCapture from "@/components/CameraCapture";
|
||||||
import BottleGrid from "@/components/BottleGrid";
|
import BottleGrid from "@/components/BottleGrid";
|
||||||
import AuthForm from "@/components/AuthForm";
|
import AuthForm from "@/components/AuthForm";
|
||||||
@@ -13,7 +13,7 @@ import LanguageSwitcher from "@/components/LanguageSwitcher";
|
|||||||
import { useI18n } from "@/i18n/I18nContext";
|
import { useI18n } from "@/i18n/I18nContext";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [bottles, setBottles] = useState<any[]>([]);
|
const [bottles, setBottles] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
@@ -64,7 +64,7 @@ export default function Home() {
|
|||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
// Listen for auth changes
|
// Listen for auth changes
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: string, session: any) => {
|
||||||
console.log('[Auth] State change event:', event, {
|
console.log('[Auth] State change event:', event, {
|
||||||
hasSession: !!session,
|
hasSession: !!session,
|
||||||
userId: session?.user?.id,
|
userId: session?.user?.id,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square } from 'lucide-react';
|
import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import AvatarStack from '@/components/AvatarStack';
|
import AvatarStack from '@/components/AvatarStack';
|
||||||
@@ -43,7 +43,7 @@ export default function SessionDetailPage() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||||
const [tastings, setTastings] = useState<SessionTasting[]>([]);
|
const [tastings, setTastings] = useState<SessionTasting[]>([]);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { LogIn, UserPlus, Mail, Lock, Loader2, AlertCircle } from 'lucide-react';
|
import { LogIn, UserPlus, Mail, Lock, Loader2, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
export default function AuthForm() {
|
export default function AuthForm() {
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [isLogin, setIsLogin] = useState(true);
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
|
||||||
export default function AuthListener() {
|
export default function AuthListener() {
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Listener für Auth-Status Änderungen
|
// Listener für Auth-Status Änderungen
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { Users, UserPlus, Trash2, User, Loader2, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Users, UserPlus, Trash2, User, Loader2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { useI18n } from '@/i18n/I18nContext';
|
import { useI18n } from '@/i18n/I18nContext';
|
||||||
|
import { addBuddy, deleteBuddy } from '@/services/buddy';
|
||||||
|
|
||||||
interface Buddy {
|
interface Buddy {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,7 +14,7 @@ interface Buddy {
|
|||||||
|
|
||||||
export default function BuddyList() {
|
export default function BuddyList() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [buddies, setBuddies] = useState<Buddy[]>([]);
|
const [buddies, setBuddies] = useState<Buddy[]>([]);
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -57,33 +58,24 @@ export default function BuddyList() {
|
|||||||
if (!newName.trim()) return;
|
if (!newName.trim()) return;
|
||||||
|
|
||||||
setIsAdding(true);
|
setIsAdding(true);
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const result = await addBuddy({ name: newName.trim() });
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
if (result.success && result.data) {
|
||||||
.from('buddies')
|
setBuddies(prev => [...[result.data], ...prev].sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
.insert([{ name: newName.trim(), user_id: user.id }])
|
|
||||||
.select();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error adding buddy:', error);
|
|
||||||
} else {
|
|
||||||
setBuddies(prev => [...(data || []), ...prev].sort((a, b) => a.name.localeCompare(b.name)));
|
|
||||||
setNewName('');
|
setNewName('');
|
||||||
|
} else {
|
||||||
|
console.error('Error adding buddy:', result.error);
|
||||||
}
|
}
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteBuddy = async (id: string) => {
|
const handleDeleteBuddy = async (id: string) => {
|
||||||
const { error } = await supabase
|
const result = await deleteBuddy(id);
|
||||||
.from('buddies')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error) {
|
if (result.success) {
|
||||||
console.error('Error deleting buddy:', error);
|
|
||||||
} else {
|
|
||||||
setBuddies(prev => prev.filter(b => b.id !== id));
|
setBuddies(prev => prev.filter(b => b.id !== id));
|
||||||
|
} else {
|
||||||
|
console.error('Error deleting buddy:', result.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Camera, Upload, CheckCircle2, AlertCircle, X, Search, ExternalLink, ArrowRight, Loader2, Wand2, Plus, Sparkles, Droplets, ChevronRight, User } from 'lucide-react';
|
import { Camera, Upload, CheckCircle2, AlertCircle, X, Search, ExternalLink, ArrowRight, Loader2, Wand2, Plus, Sparkles, Droplets, ChevronRight, User } from 'lucide-react';
|
||||||
|
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { analyzeBottle } from '@/services/analyze-bottle';
|
import { analyzeBottle } from '@/services/analyze-bottle';
|
||||||
import { saveBottle } from '@/services/save-bottle';
|
import { saveBottle } from '@/services/save-bottle';
|
||||||
@@ -27,7 +27,7 @@ interface CameraCaptureProps {
|
|||||||
|
|
||||||
export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) {
|
export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) {
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { activeSession } = useSession();
|
const { activeSession } = useSession();
|
||||||
@@ -157,12 +157,35 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
if (onAnalysisComplete) {
|
if (onAnalysisComplete) {
|
||||||
onAnalysisComplete(response.data);
|
onAnalysisComplete(response.data);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If scan fails but it looks like a network issue, offer to queue
|
||||||
|
const isNetworkError = !navigator.onLine ||
|
||||||
|
response.error?.toLowerCase().includes('fetch') ||
|
||||||
|
response.error?.toLowerCase().includes('network') ||
|
||||||
|
response.error?.toLowerCase().includes('timeout');
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
console.log('Network issue detected during scan. Queuing...');
|
||||||
|
await db.pending_scans.add({
|
||||||
|
imageBase64: compressedBase64,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
provider: aiProvider,
|
||||||
|
locale: locale
|
||||||
|
});
|
||||||
|
setIsQueued(true);
|
||||||
|
setError(null); // Clear error as we are queuing
|
||||||
} else {
|
} else {
|
||||||
setError(response.error || t('camera.analysisError'));
|
setError(response.error || t('camera.analysisError'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Processing failed:', err);
|
console.error('Processing failed:', err);
|
||||||
|
// Even on generic error, if we have a compressed image, consider queuing if it looks like connection
|
||||||
|
if (previewUrl && !analysisResult) {
|
||||||
|
setError(t('camera.processingError') + " - " + t('camera.offlineNotice'));
|
||||||
|
} else {
|
||||||
setError(t('camera.processingError'));
|
setError(t('camera.processingError'));
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -361,8 +384,20 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center">
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-md flex flex-col items-center justify-center gap-4 text-white p-6 text-center animate-in fade-in duration-300">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
<div className="relative">
|
||||||
|
<Loader2 size={48} className="animate-spin text-amber-500" />
|
||||||
|
<Wand2 size={20} className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="font-black uppercase tracking-[0.2em] text-[10px] text-amber-500">Magic Analysis</p>
|
||||||
|
<p className="text-sm font-bold">
|
||||||
|
{!navigator.onLine ? 'Offline: Speichere lokal...' : 'Analysiere Flasche...'}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-zinc-400 max-w-[200px] mx-auto leading-relaxed">
|
||||||
|
{!navigator.onLine ? 'Dein Scan wird in der Warteschlange gespeichert und synchronisiert, sobald du wieder Empfang hast.' : 'Wir suchen in der Datenbank nach Details zu deinem Whisky...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -548,9 +583,19 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isQueued && (
|
{isQueued && (
|
||||||
<div className="flex items-center gap-2 text-purple-500 text-sm bg-purple-50 dark:bg-purple-900/10 p-4 rounded-xl w-full border border-purple-100 dark:border-purple-800/30 font-medium">
|
<div className="flex flex-col gap-3 p-5 bg-gradient-to-br from-purple-500/10 to-amber-500/10 dark:from-purple-900/20 dark:to-amber-900/20 border border-purple-500/20 rounded-3xl w-full animate-in zoom-in-95 duration-500">
|
||||||
<Sparkles size={16} />
|
<div className="flex items-center gap-3">
|
||||||
{t('camera.offlineNotice')}
|
<div className="w-10 h-10 rounded-2xl bg-purple-500 flex items-center justify-center text-white shadow-lg shadow-purple-500/30">
|
||||||
|
<Sparkles size={20} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-black text-zinc-800 dark:text-zinc-100 italic">Lokal gespeichert!</span>
|
||||||
|
<span className="text-[10px] font-bold text-purple-600 dark:text-purple-400 uppercase tracking-widest">Warteschlange aktiv</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed">
|
||||||
|
Keine Sorge, dein Scan wurde sicher im Vault gespeichert. Sobald du wieder Empfang hast, wird die Analyse automatisch im Hintergrund gestartet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import AvatarStack from './AvatarStack';
|
import AvatarStack from './AvatarStack';
|
||||||
@@ -20,7 +20,7 @@ interface Session {
|
|||||||
|
|
||||||
export default function SessionList() {
|
export default function SessionList() {
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [sessions, setSessions] = useState<Session[]>([]);
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { saveTasting } from '@/services/save-tasting';
|
import { saveTasting } from '@/services/save-tasting';
|
||||||
import { Loader2, Send, Star, Users, Check, Sparkles, Droplets, Wind, Utensils, Zap } from 'lucide-react';
|
import { Loader2, Send, Star, Users, Check, Sparkles, Droplets, Wind, Utensils, Zap } from 'lucide-react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { useI18n } from '@/i18n/I18nContext';
|
import { useI18n } from '@/i18n/I18nContext';
|
||||||
import { useSession } from '@/context/SessionContext';
|
import { useSession } from '@/context/SessionContext';
|
||||||
import TagSelector from './TagSelector';
|
import TagSelector from './TagSelector';
|
||||||
@@ -22,7 +22,7 @@ interface TastingNoteFormProps {
|
|||||||
|
|
||||||
export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteFormProps) {
|
export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteFormProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [rating, setRating] = useState(85);
|
const [rating, setRating] = useState(85);
|
||||||
const [nose, setNose] = useState('');
|
const [nose, setNose] = useState('');
|
||||||
const [palate, setPalate] = useState('');
|
const [palate, setPalate] = useState('');
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { db, PendingScan, PendingTasting } from '@/lib/db';
|
|||||||
import { analyzeBottle } from '@/services/analyze-bottle';
|
import { analyzeBottle } from '@/services/analyze-bottle';
|
||||||
import { saveBottle } from '@/services/save-bottle';
|
import { saveBottle } from '@/services/save-bottle';
|
||||||
import { saveTasting } from '@/services/save-tasting';
|
import { saveTasting } from '@/services/save-tasting';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { RefreshCw, CheckCircle2, AlertCircle, Loader2, Info } from 'lucide-react';
|
import { RefreshCw, CheckCircle2, AlertCircle, Loader2, Info } from 'lucide-react';
|
||||||
|
|
||||||
export default function UploadQueue() {
|
export default function UploadQueue() {
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const [currentProgress, setCurrentProgress] = useState<{ id: string, status: string } | null>(null);
|
const [currentProgress, setCurrentProgress] = useState<{ id: string, status: string } | null>(null);
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
const [completedItems, setCompletedItems] = useState<{ id: string; name: string; bottleId?: string; type: 'scan' | 'tasting' }[]>([]);
|
||||||
|
|
||||||
const pendingScans = useLiveQuery(() => db.pending_scans.toArray(), [], [] as PendingScan[]);
|
const pendingScans = useLiveQuery(() => db.pending_scans.toArray(), [], [] as PendingScan[]);
|
||||||
const pendingTastings = useLiveQuery(() => db.pending_tastings.toArray(), [], [] as PendingTasting[]);
|
const pendingTastings = useLiveQuery(() => db.pending_tastings.toArray(), [], [] as PendingTasting[]);
|
||||||
@@ -39,9 +41,16 @@ export default function UploadQueue() {
|
|||||||
try {
|
try {
|
||||||
const analysis = await analyzeBottle(item.imageBase64, undefined, item.locale);
|
const analysis = await analyzeBottle(item.imageBase64, undefined, item.locale);
|
||||||
if (analysis.success && analysis.data) {
|
if (analysis.success && analysis.data) {
|
||||||
|
const bottleData = analysis.data;
|
||||||
setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' });
|
setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' });
|
||||||
const save = await saveBottle(analysis.data, item.imageBase64, user.id);
|
const save = await saveBottle(bottleData, item.imageBase64, user.id);
|
||||||
if (save.success) {
|
if (save.success && save.data) {
|
||||||
|
setCompletedItems(prev => [...prev.slice(-4), {
|
||||||
|
id: itemId,
|
||||||
|
name: bottleData.name || 'Unbekannter Whisky',
|
||||||
|
bottleId: save.data.id,
|
||||||
|
type: 'scan'
|
||||||
|
}]);
|
||||||
await db.pending_scans.delete(item.id!);
|
await db.pending_scans.delete(item.id!);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,10 +71,17 @@ export default function UploadQueue() {
|
|||||||
try {
|
try {
|
||||||
const result = await saveTasting({
|
const result = await saveTasting({
|
||||||
...item.data,
|
...item.data,
|
||||||
|
is_sample: item.data.is_sample ?? false,
|
||||||
bottle_id: item.bottle_id,
|
bottle_id: item.bottle_id,
|
||||||
tasted_at: item.tasted_at
|
tasted_at: item.tasted_at
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
setCompletedItems(prev => [...prev.slice(-4), {
|
||||||
|
id: itemId,
|
||||||
|
name: 'Tasting Note',
|
||||||
|
bottleId: item.bottle_id,
|
||||||
|
type: 'tasting'
|
||||||
|
}]);
|
||||||
await db.pending_tastings.delete(item.id!);
|
await db.pending_tastings.delete(item.id!);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
@@ -103,50 +119,116 @@ export default function UploadQueue() {
|
|||||||
if (totalInQueue === 0) return null;
|
if (totalInQueue === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-6 right-6 z-50 animate-in slide-in-from-right-10">
|
<div className={`fixed bottom-6 left-1/2 -translate-x-1/2 md:left-auto md:translate-x-0 md:right-6 z-[100] animate-in slide-in-from-bottom-10 md:slide-in-from-right-10 w-[calc(100%-2rem)] md:w-auto`}>
|
||||||
<div className="bg-zinc-900 text-white p-4 rounded-2xl shadow-2xl border border-white/10 flex flex-col gap-3 min-w-[300px]">
|
<div className="bg-zinc-900 border border-white/10 rounded-3xl shadow-2xl overflow-hidden transition-all duration-500">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 pb-2">
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
<RefreshCw size={16} className={isSyncing ? 'animate-spin text-amber-500' : 'text-zinc-400'} />
|
className="p-4 bg-zinc-800/50 flex items-center justify-between cursor-pointer"
|
||||||
<span className="text-xs font-black uppercase tracking-widest">Global Sync Queue</span>
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<RefreshCw size={18} className={isSyncing ? 'animate-spin text-amber-500' : 'text-zinc-500'} />
|
||||||
|
{totalInQueue > 0 && !isSyncing && (
|
||||||
|
<div className="absolute -top-1 -right-1 w-2 h-2 bg-amber-500 rounded-full animate-pulse" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="bg-amber-600 text-[10px] font-black px-1.5 py-0.5 rounded-md">
|
<div className="flex flex-col">
|
||||||
{totalInQueue} Items
|
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400">Sync Warteschlange</span>
|
||||||
|
<span className="text-xs font-bold text-white">
|
||||||
|
{isSyncing ? 'Synchronisiere...' : navigator.onLine ? 'Warten auf Upload' : 'Offline - Lokal gespeichert'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="bg-amber-600 text-[10px] font-black px-2 py-0.5 rounded-full text-white">
|
||||||
|
{totalInQueue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Content */}
|
||||||
{/* Scans */}
|
{!isCollapsed && (
|
||||||
{pendingScans.map((item) => (
|
<div className="p-4 space-y-4 max-h-[400px] overflow-y-auto custom-scrollbar">
|
||||||
<div key={`scan-${item.id}`} className="flex items-center justify-between text-[11px] font-medium text-zinc-400">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
{/* Completed Items (The "Results") */}
|
||||||
<div className="w-6 h-6 rounded bg-zinc-800 overflow-hidden ring-1 ring-white/10">
|
{completedItems.length > 0 && (
|
||||||
<img src={item.imageBase64} className="w-full h-full object-cover opacity-50" />
|
<div className="space-y-2 pb-2 border-b border-white/5">
|
||||||
|
<div className="flex items-center gap-2 text-[9px] font-black uppercase tracking-[0.2em] text-green-500 mb-2">
|
||||||
|
<CheckCircle2 size={10} />
|
||||||
|
Synchronisierte Items
|
||||||
</div>
|
</div>
|
||||||
<span className="truncate max-w-[150px]">
|
{completedItems.map((item) => (
|
||||||
{currentProgress?.id === `scan-${item.id}` ? currentProgress.status : 'Scan wartet...'}
|
<div key={`done-${item.id}`} className="flex items-center justify-between p-2 rounded-xl bg-green-500/10 border border-green-500/20 animate-in zoom-in-95">
|
||||||
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<span className="text-[10px] font-black text-green-500 uppercase tracking-widest">
|
||||||
|
{item.type === 'scan' ? 'Neu im Vault' : 'Tasting gespeichert'}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-bold text-white truncate pr-2">
|
||||||
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{item.bottleId && (
|
||||||
|
<a
|
||||||
|
href={`/bottles/${item.bottleId}`}
|
||||||
|
className="shrink-0 px-3 py-1.5 bg-green-500 hover:bg-green-600 text-white text-[10px] font-black uppercase rounded-lg transition-all shadow-lg shadow-green-500/20 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Ansehen
|
||||||
|
<CheckCircle2 size={10} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scans */}
|
||||||
|
{pendingScans.map((item) => (
|
||||||
|
<div key={`scan-${item.id}`} className="group flex items-center justify-between p-2 rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-colors">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-zinc-800 overflow-hidden ring-1 ring-white/10 shrink-0">
|
||||||
|
<img src={item.imageBase64} className="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-[10px] font-black text-amber-500/80 uppercase tracking-widest">Magic Shot</span>
|
||||||
|
<span className="text-[11px] font-medium text-zinc-300">
|
||||||
{currentProgress?.id === `scan-${item.id}` ? (
|
{currentProgress?.id === `scan-${item.id}` ? (
|
||||||
<Loader2 size={12} className="animate-spin text-amber-500" />
|
<span className="flex items-center gap-1.5">
|
||||||
) : <Info size={12} className="text-zinc-600" />}
|
<Loader2 size={10} className="animate-spin" />
|
||||||
|
{currentProgress.status}
|
||||||
|
</span>
|
||||||
|
) : 'Wartet auf Verbindung...'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[9px] text-zinc-500 font-bold whitespace-nowrap ml-4">
|
||||||
|
{new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Tastings */}
|
{/* Tastings */}
|
||||||
{pendingTastings.map((item) => (
|
{pendingTastings.map((item) => (
|
||||||
<div key={`tasting-${item.id}`} className="flex items-center justify-between text-[11px] font-medium text-zinc-400">
|
<div key={`tasting-${item.id}`} className="group flex items-center justify-between p-2 rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-colors">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-6 h-6 rounded-full bg-amber-900/40 flex items-center justify-center text-[8px] font-bold text-amber-500 ring-1 ring-amber-500/20">
|
<div className="w-10 h-10 rounded-lg bg-amber-900/20 flex items-center justify-center ring-1 ring-amber-500/20 shrink-0">
|
||||||
{item.data.rating}
|
<div className="text-sm font-black text-amber-500">{item.data.rating}</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="truncate max-w-[150px]">
|
<div className="flex flex-col gap-0.5">
|
||||||
{currentProgress?.id === `tasting-${item.id}` ? currentProgress.status : 'Tasting wartet...'}
|
<span className="text-[10px] font-black text-amber-500/80 uppercase tracking-widest">Tasting Node</span>
|
||||||
|
<span className="text-[11px] font-medium text-zinc-300">
|
||||||
|
{currentProgress?.id === `tasting-${item.id}` ? (
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Loader2 size={10} className="animate-spin" />
|
||||||
|
{currentProgress.status}
|
||||||
|
</span>
|
||||||
|
) : 'Wartet auf Sync...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{currentProgress?.id === `tasting-${item.id}` ? (
|
</div>
|
||||||
<Loader2 size={12} className="animate-spin text-amber-500" />
|
<div className="text-[9px] text-zinc-500 font-bold whitespace-nowrap ml-4">
|
||||||
) : <Info size={12} className="text-zinc-600" />}
|
{new Date(item.tasted_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,12 +236,21 @@ export default function UploadQueue() {
|
|||||||
{navigator.onLine && !isSyncing && (
|
{navigator.onLine && !isSyncing && (
|
||||||
<button
|
<button
|
||||||
onClick={() => syncQueue()}
|
onClick={() => syncQueue()}
|
||||||
className="w-full py-2.5 bg-zinc-800 hover:bg-zinc-700 text-amber-500 text-[10px] font-black uppercase rounded-xl transition-all border border-white/5 active:scale-95 flex items-center justify-center gap-2 mt-1"
|
className="w-full py-3 bg-white/5 hover:bg-white/10 text-amber-500 text-[10px] font-black uppercase rounded-2xl transition-all border border-white/5 active:scale-95 flex items-center justify-center gap-2 mt-2"
|
||||||
>
|
>
|
||||||
<RefreshCw size={12} />
|
<RefreshCw size={12} />
|
||||||
Sync Erzwingen
|
Synchronisierung erzwingen
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!navigator.onLine && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-2 text-[10px] text-zinc-500 font-bold italic">
|
||||||
|
<AlertCircle size={12} />
|
||||||
|
Keine Internetverbindung
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { getAllSystemTags } from '@/services/tags';
|
import { getAllSystemTags } from '@/services/tags';
|
||||||
|
|
||||||
export function useCacheSync() {
|
export function useCacheSync() {
|
||||||
const supabase = createClientComponentClient();
|
const supabase = createClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncCache = async () => {
|
const syncCache = async () => {
|
||||||
|
|||||||
8
src/lib/supabase/client.ts
Normal file
8
src/lib/supabase/client.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createBrowserClient } from '@supabase/ssr';
|
||||||
|
|
||||||
|
export function createClient() {
|
||||||
|
return createBrowserClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/lib/supabase/server.ts
Normal file
29
src/lib/supabase/server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
|
export async function createClient() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
|
||||||
|
return createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return cookieStore.getAll();
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
|
||||||
|
try {
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
cookieStore.set(name, value, options)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// The `setAll` method was called from a Server Component.
|
||||||
|
// This can be ignored if you have middleware refreshing
|
||||||
|
// user sessions.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
export async function middleware(req: NextRequest) {
|
|
||||||
const res = NextResponse.next();
|
|
||||||
const url = new URL(req.url);
|
|
||||||
|
|
||||||
// Skip logs for static assets
|
|
||||||
const isStatic = url.pathname.startsWith('/_next') || url.pathname.includes('/icon-') || url.pathname === '/favicon.ico';
|
|
||||||
|
|
||||||
// Only attempt session refresh if variables are present
|
|
||||||
if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
||||||
try {
|
|
||||||
const supabase = createMiddlewareClient({ req, res });
|
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
|
||||||
|
|
||||||
if (!isStatic) {
|
|
||||||
const status = session ? `User:${session.user.id.slice(0, 8)}` : 'No Session';
|
|
||||||
console.log(`[MW] ${req.method} ${url.pathname} | ${status}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[MW] Auth Error:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
/*
|
|
||||||
* Match all request paths except for the ones starting with:
|
|
||||||
* - _next/static (static files)
|
|
||||||
* - _next/image (image optimization files)
|
|
||||||
* - favicon.ico (favicon file)
|
|
||||||
* Feel free to modify this pattern to include more paths.
|
|
||||||
*/
|
|
||||||
'/((?!_next/static|_next/image|favicon.ico).*)',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
53
src/proxy.ts
Normal file
53
src/proxy.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
||||||
|
import { NextResponse, type NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export async function proxy(request: NextRequest) {
|
||||||
|
let response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: request.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const supabase = createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return request.cookies.getAll();
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
request.cookies.set(name, value)
|
||||||
|
);
|
||||||
|
response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: request.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
response.cookies.set(name, value, options)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const isStatic = url.pathname.startsWith('/_next') || url.pathname.includes('/icon-') || url.pathname === '/favicon.ico';
|
||||||
|
|
||||||
|
if (!isStatic) {
|
||||||
|
const status = session ? `User:${session.user.id.slice(0, 8)}` : 'No Session';
|
||||||
|
console.log(`[Proxy] ${request.method} ${url.pathname} | ${status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { checkIsAdmin } from './track-api-usage';
|
import { checkIsAdmin } from './track-api-usage';
|
||||||
import { addCredits, getUserCredits } from './credit-service';
|
import { addCredits, getUserCredits } from './credit-service';
|
||||||
|
import { AdminCreditUpdateSchema, AdminSettingsSchema } from '@/types/whisky';
|
||||||
|
|
||||||
interface UserWithCredits {
|
interface UserWithCredits {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,7 +23,7 @@ interface UserWithCredits {
|
|||||||
*/
|
*/
|
||||||
export async function getAllUsersWithCredits(): Promise<UserWithCredits[]> {
|
export async function getAllUsersWithCredits(): Promise<UserWithCredits[]> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -97,7 +97,8 @@ export async function updateUserCredits(
|
|||||||
reason: string
|
reason: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const validated = AdminCreditUpdateSchema.parse({ userId, newBalance, reason });
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -107,20 +108,20 @@ export async function updateUserCredits(
|
|||||||
if (!isAdmin) return { success: false, error: 'Not authorized' };
|
if (!isAdmin) return { success: false, error: 'Not authorized' };
|
||||||
|
|
||||||
// Get current credits
|
// Get current credits
|
||||||
const currentCredits = await getUserCredits(userId);
|
const currentCredits = await getUserCredits(validated.userId);
|
||||||
if (!currentCredits) {
|
if (!currentCredits) {
|
||||||
return { success: false, error: 'User credits not found' };
|
return { success: false, error: 'User credits not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const difference = newBalance - currentCredits.balance;
|
const difference = validated.newBalance - currentCredits.balance;
|
||||||
|
|
||||||
// Use addCredits which handles the transaction logging
|
// Use addCredits which handles the transaction logging
|
||||||
const result = await addCredits(userId, difference, reason, user.id);
|
const result = await addCredits(validated.userId, difference, validated.reason, user.id);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in updateUserCredits:', err);
|
console.error('Error in updateUserCredits:', err);
|
||||||
return { success: false, error: 'Failed to update credits' };
|
return { success: false, error: err instanceof Error ? err.message : 'Failed to update credits' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,8 @@ export async function setUserDailyLimit(
|
|||||||
dailyLimit: number | null
|
dailyLimit: number | null
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const validated = AdminSettingsSchema.parse({ userId, dailyLimit });
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -143,8 +145,8 @@ export async function setUserDailyLimit(
|
|||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('user_credits')
|
.from('user_credits')
|
||||||
.update({ daily_limit: dailyLimit })
|
.update({ daily_limit: validated.dailyLimit })
|
||||||
.eq('user_id', userId);
|
.eq('user_id', validated.userId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error setting daily limit:', error);
|
console.error('Error setting daily limit:', error);
|
||||||
@@ -154,7 +156,7 @@ export async function setUserDailyLimit(
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in setUserDailyLimit:', err);
|
console.error('Error in setUserDailyLimit:', err);
|
||||||
return { success: false, error: 'Failed to set daily limit' };
|
return { success: false, error: err instanceof Error ? err.message : 'Failed to set daily limit' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +169,8 @@ export async function setUserApiCosts(
|
|||||||
geminiAiCost: number
|
geminiAiCost: number
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const validated = AdminSettingsSchema.parse({ userId, googleSearchCost, geminiAiCost });
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -179,10 +182,10 @@ export async function setUserApiCosts(
|
|||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('user_credits')
|
.from('user_credits')
|
||||||
.update({
|
.update({
|
||||||
google_search_cost: googleSearchCost,
|
google_search_cost: validated.googleSearchCost,
|
||||||
gemini_ai_cost: geminiAiCost
|
gemini_ai_cost: validated.geminiAiCost
|
||||||
})
|
})
|
||||||
.eq('user_id', userId);
|
.eq('user_id', validated.userId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error setting API costs:', error);
|
console.error('Error setting API costs:', error);
|
||||||
@@ -192,7 +195,7 @@ export async function setUserApiCosts(
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error in setUserApiCosts:', err);
|
console.error('Error in setUserApiCosts:', err);
|
||||||
return { success: false, error: 'Failed to set API costs' };
|
return { success: false, error: err instanceof Error ? err.message : 'Failed to set API costs' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +208,7 @@ export async function bulkAddCredits(
|
|||||||
reason: string
|
reason: string
|
||||||
): Promise<{ success: boolean; processed: number; failed: number; error?: string }> {
|
): Promise<{ success: boolean; processed: number; failed: number; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import { getNebiusClient } from '@/lib/ai-client';
|
import { getNebiusClient } from '@/lib/ai-client';
|
||||||
import { SYSTEM_INSTRUCTION as GEMINI_SYSTEM_INSTRUCTION } from '@/lib/gemini';
|
import { SYSTEM_INSTRUCTION as GEMINI_SYSTEM_INSTRUCTION } from '@/lib/gemini';
|
||||||
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { trackApiUsage } from './track-api-usage';
|
import { trackApiUsage } from './track-api-usage';
|
||||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||||
@@ -16,7 +15,7 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[],
|
|||||||
|
|
||||||
let supabase;
|
let supabase;
|
||||||
try {
|
try {
|
||||||
supabase = createServerActionClient({ cookies });
|
supabase = await createClient();
|
||||||
console.log('[analyzeBottleNebius] Initialized Supabase client');
|
console.log('[analyzeBottleNebius] Initialized Supabase client');
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { geminiModel, SYSTEM_INSTRUCTION } from '@/lib/gemini';
|
import { geminiModel, SYSTEM_INSTRUCTION } from '@/lib/gemini';
|
||||||
import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
|
import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { trackApiUsage } from './track-api-usage';
|
import { trackApiUsage } from './track-api-usage';
|
||||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||||
@@ -16,7 +15,7 @@ export async function analyzeBottle(base64Image: string, tags?: string[], locale
|
|||||||
let supabase; // Declare supabase outside try block for error tracking access
|
let supabase; // Declare supabase outside try block for error tracking access
|
||||||
try {
|
try {
|
||||||
// Initialize Supabase client inside the try block
|
// Initialize Supabase client inside the try block
|
||||||
supabase = createServerActionClient({ cookies });
|
supabase = await createClient();
|
||||||
console.log('[analyzeBottle] Initialized Supabase client');
|
console.log('[analyzeBottle] Initialized Supabase client');
|
||||||
|
|
||||||
// ... (auth and credit check remain same) ...
|
// ... (auth and credit check remain same) ...
|
||||||
|
|||||||
53
src/services/buddy.ts
Normal file
53
src/services/buddy.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { createClient } from '@/lib/supabase/server';
|
||||||
|
import { BuddySchema, BuddyData } from '@/types/whisky';
|
||||||
|
|
||||||
|
export async function addBuddy(rawData: BuddyData) {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { name } = BuddySchema.parse(rawData);
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('buddies')
|
||||||
|
.insert([{ name, user_id: session.user.id }])
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return { success: true, data };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Add Buddy Error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Buddies',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteBuddy(id: string) {
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('buddies')
|
||||||
|
.delete()
|
||||||
|
.eq('id', id)
|
||||||
|
.eq('user_id', session.user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete Buddy Error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Buddies',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
import { checkIsAdmin } from './track-api-usage';
|
import { checkIsAdmin } from './track-api-usage';
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ interface CreditTransaction {
|
|||||||
*/
|
*/
|
||||||
export async function getUserCredits(userId: string): Promise<UserCredits | null> {
|
export async function getUserCredits(userId: string): Promise<UserCredits | null> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Security check: Only self or admin can view credits
|
// Security check: Only self or admin can view credits
|
||||||
const { data: { user: currentUser } } = await supabase.auth.getUser();
|
const { data: { user: currentUser } } = await supabase.auth.getUser();
|
||||||
@@ -137,7 +136,7 @@ export async function deductCredits(
|
|||||||
reason?: string
|
reason?: string
|
||||||
): Promise<{ success: boolean; newBalance?: number; error?: string }> {
|
): Promise<{ success: boolean; newBalance?: number; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Get current credits
|
// Get current credits
|
||||||
const credits = await getUserCredits(userId);
|
const credits = await getUserCredits(userId);
|
||||||
@@ -218,7 +217,7 @@ export async function addCredits(
|
|||||||
adminId?: string
|
adminId?: string
|
||||||
): Promise<{ success: boolean; newBalance?: number; error?: string }> {
|
): Promise<{ success: boolean; newBalance?: number; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Security check
|
// Security check
|
||||||
const { data: { user: currentUser } } = await supabase.auth.getUser();
|
const { data: { user: currentUser } } = await supabase.auth.getUser();
|
||||||
@@ -284,7 +283,7 @@ export async function getCreditTransactions(
|
|||||||
limit: number = 50
|
limit: number = 50
|
||||||
): Promise<CreditTransaction[]> {
|
): Promise<CreditTransaction[]> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('credit_transactions')
|
.from('credit_transactions')
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
export async function deleteBottle(bottleId: string) {
|
export async function deleteBottle(bottleId: string) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
export async function deleteSession(sessionId: string) {
|
export async function deleteSession(sessionId: string) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
export async function deleteTasting(tastingId: string, bottleId: string) {
|
export async function deleteTasting(tastingId: string, bottleId: string) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
import { trackApiUsage, checkDailyLimit } from './track-api-usage';
|
import { trackApiUsage, checkDailyLimit } from './track-api-usage';
|
||||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
import { DiscoveryDataSchema, DiscoveryData } from '@/types/whisky';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to discover a Whiskybase ID for a given bottle.
|
* Service to discover a Whiskybase ID for a given bottle.
|
||||||
* Uses Google Custom Search JSON API to search Google and extracts the ID from the first result.
|
* Uses Google Custom Search JSON API to search Google and extracts the ID from the first result.
|
||||||
*/
|
*/
|
||||||
export async function discoverWhiskybaseId(bottle: {
|
export async function discoverWhiskybaseId(rawBottle: DiscoveryData) {
|
||||||
name: string;
|
// Validate input
|
||||||
distillery?: string;
|
const bottle = DiscoveryDataSchema.parse(rawBottle);
|
||||||
abv?: number;
|
|
||||||
age?: number;
|
|
||||||
distilled_at?: string;
|
|
||||||
bottled_at?: string;
|
|
||||||
batch_info?: string;
|
|
||||||
}) {
|
|
||||||
// Both Gemini and Custom Search often use the same API key if created via AI Studio
|
// Both Gemini and Custom Search often use the same API key if created via AI Studio
|
||||||
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
||||||
const cx = '37e905eb03fd14e0f'; // Provided by user
|
const cx = '37e905eb03fd14e0f'; // Provided by user
|
||||||
@@ -30,7 +25,7 @@ export async function discoverWhiskybaseId(bottle: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current user for tracking
|
// Get current user for tracking
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { BottleMetadata } from '@/types/whisky';
|
import { BottleMetadata } from '@/types/whisky';
|
||||||
|
|
||||||
export async function findMatchingBottle(metadata: BottleMetadata) {
|
export async function findMatchingBottle(metadata: BottleMetadata) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
import { BottleMetadataSchema } from '@/types/whisky';
|
||||||
import { BottleMetadata } from '@/types/whisky';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
export async function saveBottle(
|
export async function saveBottle(
|
||||||
metadata: BottleMetadata,
|
rawMetadata: any,
|
||||||
base64Image: string | null,
|
base64Image: string | null,
|
||||||
_ignoredUserId: string, // Keeping for signature compatibility
|
_ignoredUserId: string, // Keeping for signature compatibility
|
||||||
preUploadedUrl?: string
|
preUploadedUrl?: string
|
||||||
) {
|
) {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const metadata = BottleMetadataSchema.parse(rawMetadata);
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error('Nicht autorisiert oder Session abgelaufen.');
|
throw new Error('Nicht autorisiert oder Session abgelaufen.');
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { validateSession } from './validate-session';
|
import { validateSession } from './validate-session';
|
||||||
|
|
||||||
export async function saveTasting(data: {
|
import { TastingNoteSchema, TastingNoteData } from '@/types/whisky';
|
||||||
bottle_id: string;
|
|
||||||
session_id?: string;
|
export async function saveTasting(rawData: TastingNoteData) {
|
||||||
rating: number;
|
const supabase = await createClient();
|
||||||
nose_notes?: string;
|
|
||||||
palate_notes?: string;
|
|
||||||
finish_notes?: string;
|
|
||||||
is_sample?: boolean;
|
|
||||||
buddy_ids?: string[];
|
|
||||||
tag_ids?: string[];
|
|
||||||
tasted_at?: string;
|
|
||||||
}) {
|
|
||||||
const supabase = createServerActionClient({ cookies });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const data = TastingNoteSchema.parse(rawData);
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) throw new Error('Nicht autorisiert');
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { checkIsAdmin } from './track-api-usage';
|
import { checkIsAdmin } from './track-api-usage';
|
||||||
import { addCredits } from './credit-service';
|
import { addCredits } from './credit-service';
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ export interface UserSubscription {
|
|||||||
export async function getAllPlans(): Promise<SubscriptionPlan[]> {
|
export async function getAllPlans(): Promise<SubscriptionPlan[]> {
|
||||||
try {
|
try {
|
||||||
console.log('[getAllPlans] Starting...');
|
console.log('[getAllPlans] Starting...');
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// RLS policy will handle admin check
|
// RLS policy will handle admin check
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -61,7 +60,7 @@ export async function getAllPlans(): Promise<SubscriptionPlan[]> {
|
|||||||
*/
|
*/
|
||||||
export async function getActivePlans(): Promise<SubscriptionPlan[]> {
|
export async function getActivePlans(): Promise<SubscriptionPlan[]> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('subscription_plans')
|
.from('subscription_plans')
|
||||||
@@ -86,7 +85,7 @@ export async function getActivePlans(): Promise<SubscriptionPlan[]> {
|
|||||||
*/
|
*/
|
||||||
export async function createPlan(plan: Omit<SubscriptionPlan, 'id' | 'created_at' | 'updated_at'>): Promise<{ success: boolean; error?: string; plan?: SubscriptionPlan }> {
|
export async function createPlan(plan: Omit<SubscriptionPlan, 'id' | 'created_at' | 'updated_at'>): Promise<{ success: boolean; error?: string; plan?: SubscriptionPlan }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -118,7 +117,7 @@ export async function createPlan(plan: Omit<SubscriptionPlan, 'id' | 'created_at
|
|||||||
*/
|
*/
|
||||||
export async function updatePlan(planId: string, updates: Partial<SubscriptionPlan>): Promise<{ success: boolean; error?: string }> {
|
export async function updatePlan(planId: string, updates: Partial<SubscriptionPlan>): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -149,7 +148,7 @@ export async function updatePlan(planId: string, updates: Partial<SubscriptionPl
|
|||||||
*/
|
*/
|
||||||
export async function deletePlan(planId: string): Promise<{ success: boolean; error?: string }> {
|
export async function deletePlan(planId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -180,7 +179,7 @@ export async function deletePlan(planId: string): Promise<{ success: boolean; er
|
|||||||
*/
|
*/
|
||||||
export async function getUserSubscription(userId: string): Promise<{ subscription: UserSubscription | null; plan: SubscriptionPlan | null }> {
|
export async function getUserSubscription(userId: string): Promise<{ subscription: UserSubscription | null; plan: SubscriptionPlan | null }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data: subscription, error: subError } = await supabase
|
const { data: subscription, error: subError } = await supabase
|
||||||
.from('user_subscriptions')
|
.from('user_subscriptions')
|
||||||
@@ -218,7 +217,7 @@ export async function getUserSubscription(userId: string): Promise<{ subscriptio
|
|||||||
*/
|
*/
|
||||||
export async function setUserPlan(userId: string, planId: string): Promise<{ success: boolean; error?: string }> {
|
export async function setUserPlan(userId: string, planId: string): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -253,7 +252,7 @@ export async function setUserPlan(userId: string, planId: string): Promise<{ suc
|
|||||||
*/
|
*/
|
||||||
export async function grantMonthlyCredits(): Promise<{ success: boolean; processed: number; failed: number; error?: string }> {
|
export async function grantMonthlyCredits(): Promise<{ success: boolean; processed: number; failed: number; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if current user is admin
|
// Check if current user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
import { TagSchema, TagData } from '@/types/whisky';
|
||||||
|
|
||||||
export type TagCategory = 'nose' | 'taste' | 'finish' | 'texture';
|
export type TagCategory = 'nose' | 'taste' | 'finish' | 'texture';
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export interface Tag {
|
|||||||
*/
|
*/
|
||||||
export async function getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
export async function getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('tags')
|
.from('tags')
|
||||||
@@ -45,7 +46,7 @@ export async function getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
|||||||
*/
|
*/
|
||||||
export async function getAllSystemTags(): Promise<Tag[]> {
|
export async function getAllSystemTags(): Promise<Tag[]> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('tags')
|
.from('tags')
|
||||||
@@ -68,10 +69,11 @@ export async function getAllSystemTags(): Promise<Tag[]> {
|
|||||||
/**
|
/**
|
||||||
* Create a custom user tag
|
* Create a custom user tag
|
||||||
*/
|
*/
|
||||||
export async function createCustomTag(name: string, category: TagCategory): Promise<{ success: boolean; tag?: Tag; error?: string }> {
|
export async function createCustomTag(rawName: string, rawCategory: TagCategory): Promise<{ success: boolean; tag?: Tag; error?: string }> {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { name, category } = TagSchema.parse({ name: rawName, category: rawCategory });
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) throw new Error('Nicht autorisiert');
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
interface TrackApiUsageParams {
|
interface TrackApiUsageParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -33,7 +32,7 @@ const GOOGLE_SEARCH_DAILY_LIMIT = 80;
|
|||||||
*/
|
*/
|
||||||
export async function trackApiUsage(params: TrackApiUsageParams): Promise<{ success: boolean; error?: string }> {
|
export async function trackApiUsage(params: TrackApiUsageParams): Promise<{ success: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Security check: Ensure user is only tracking their own usage
|
// Security check: Ensure user is only tracking their own usage
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -69,7 +68,7 @@ export async function trackApiUsage(params: TrackApiUsageParams): Promise<{ succ
|
|||||||
*/
|
*/
|
||||||
export async function checkDailyLimit(apiType: 'google_search' | 'gemini_ai'): Promise<DailyLimitCheck> {
|
export async function checkDailyLimit(apiType: 'google_search' | 'gemini_ai'): Promise<DailyLimitCheck> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Only enforce limit for Google Search
|
// Only enforce limit for Google Search
|
||||||
if (apiType !== 'google_search') {
|
if (apiType !== 'google_search') {
|
||||||
@@ -114,7 +113,7 @@ export async function checkDailyLimit(apiType: 'google_search' | 'gemini_ai'): P
|
|||||||
*/
|
*/
|
||||||
export async function getUserApiStats(userId: string): Promise<ApiStats | null> {
|
export async function getUserApiStats(userId: string): Promise<ApiStats | null> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('api_usage')
|
.from('api_usage')
|
||||||
@@ -152,7 +151,7 @@ export async function getUserApiStats(userId: string): Promise<ApiStats | null>
|
|||||||
*/
|
*/
|
||||||
export async function getGlobalApiStats(): Promise<ApiStats | null> {
|
export async function getGlobalApiStats(): Promise<ApiStats | null> {
|
||||||
try {
|
try {
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
@@ -197,7 +196,7 @@ export async function getGlobalApiStats(): Promise<ApiStats | null> {
|
|||||||
export async function checkIsAdmin(userId: string): Promise<boolean> {
|
export async function checkIsAdmin(userId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
console.log('[checkIsAdmin] Checking admin status for user:', userId);
|
console.log('[checkIsAdmin] Checking admin status for user:', userId);
|
||||||
const supabase = createServerComponentClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('admin_users')
|
.from('admin_users')
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
export async function updateBottleStatus(bottleId: string, status: 'sealed' | 'open' | 'sampled' | 'empty') {
|
export async function updateBottleStatus(bottleId: string, status: 'sealed' | 'open' | 'sampled' | 'empty') {
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
export async function updateBottle(bottleId: string, data: {
|
import { UpdateBottleSchema, UpdateBottleData } from '@/types/whisky';
|
||||||
name?: string;
|
|
||||||
distillery?: string;
|
export async function updateBottle(bottleId: string, rawData: UpdateBottleData) {
|
||||||
category?: string;
|
const supabase = await createClient();
|
||||||
abv?: number;
|
|
||||||
age?: number;
|
|
||||||
whiskybase_id?: string;
|
|
||||||
purchase_price?: number;
|
|
||||||
distilled_at?: string;
|
|
||||||
bottled_at?: string;
|
|
||||||
batch_info?: string;
|
|
||||||
}) {
|
|
||||||
const supabase = createServerActionClient({ cookies });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const data = UpdateBottleSchema.parse(rawData);
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
if (!session) throw new Error('Nicht autorisiert');
|
if (!session) throw new Error('Nicht autorisiert');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates if a session is still "active" based on its age.
|
* Validates if a session is still "active" based on its age.
|
||||||
@@ -10,7 +9,7 @@ import { cookies } from 'next/headers';
|
|||||||
export async function validateSession(sessionId: string | null): Promise<boolean> {
|
export async function validateSession(sessionId: string | null): Promise<boolean> {
|
||||||
if (!sessionId) return false;
|
if (!sessionId) return false;
|
||||||
|
|
||||||
const supabase = createServerActionClient({ cookies });
|
const supabase = await createClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: session, error } = await supabase
|
const { data: session, error } = await supabase
|
||||||
|
|||||||
@@ -1,25 +1,97 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const BottleMetadataSchema = z.object({
|
export const BottleMetadataSchema = z.object({
|
||||||
name: z.string().nullish(),
|
name: z.string().trim().min(1).max(255).nullish(),
|
||||||
distillery: z.string().nullish(),
|
distillery: z.string().trim().max(255).nullish(),
|
||||||
category: z.string().nullish(),
|
category: z.string().trim().max(100).nullish(),
|
||||||
abv: z.number().nullish(),
|
abv: z.number().min(0).max(100).nullish(),
|
||||||
age: z.number().nullish(),
|
age: z.number().min(0).max(100).nullish(),
|
||||||
vintage: z.string().nullish(),
|
vintage: z.string().trim().max(50).nullish(),
|
||||||
bottleCode: z.string().nullish(),
|
bottleCode: z.string().trim().max(100).nullish(),
|
||||||
whiskybaseId: z.string().nullish(),
|
whiskybaseId: z.string().trim().max(50).nullish(),
|
||||||
distilled_at: z.string().nullish(),
|
distilled_at: z.string().trim().max(50).nullish(),
|
||||||
bottled_at: z.string().nullish(),
|
bottled_at: z.string().trim().max(50).nullish(),
|
||||||
batch_info: z.string().nullish(),
|
batch_info: z.string().trim().max(255).nullish(),
|
||||||
is_whisky: z.boolean().default(true),
|
is_whisky: z.boolean().default(true),
|
||||||
confidence: z.number().min(0).max(100).default(100),
|
confidence: z.number().min(0).max(100).default(100),
|
||||||
suggested_tags: z.array(z.string()).nullish(),
|
suggested_tags: z.array(z.string().trim().max(100)).nullish(),
|
||||||
suggested_custom_tags: z.array(z.string()).nullish(),
|
suggested_custom_tags: z.array(z.string().trim().max(100)).nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BottleMetadata = z.infer<typeof BottleMetadataSchema>;
|
export type BottleMetadata = z.infer<typeof BottleMetadataSchema>;
|
||||||
|
|
||||||
|
export const TastingNoteSchema = z.object({
|
||||||
|
bottle_id: z.string().uuid(),
|
||||||
|
session_id: z.string().uuid().optional(),
|
||||||
|
rating: z.number().min(0).max(100),
|
||||||
|
nose_notes: z.string().trim().max(2000).optional(),
|
||||||
|
palate_notes: z.string().trim().max(2000).optional(),
|
||||||
|
finish_notes: z.string().trim().max(2000).optional(),
|
||||||
|
is_sample: z.boolean().default(false),
|
||||||
|
buddy_ids: z.array(z.string().uuid()).optional(),
|
||||||
|
tag_ids: z.array(z.string().uuid()).optional(),
|
||||||
|
tasted_at: z.string().datetime().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TastingNoteData = z.infer<typeof TastingNoteSchema>;
|
||||||
|
|
||||||
|
export const UpdateBottleSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(255).optional(),
|
||||||
|
distillery: z.string().trim().max(255).optional(),
|
||||||
|
category: z.string().trim().max(100).optional(),
|
||||||
|
abv: z.number().min(0).max(100).optional(),
|
||||||
|
age: z.number().min(0).max(100).optional(),
|
||||||
|
whiskybase_id: z.string().trim().max(50).optional(),
|
||||||
|
purchase_price: z.number().min(0).optional(),
|
||||||
|
distilled_at: z.string().trim().max(50).optional(),
|
||||||
|
bottled_at: z.string().trim().max(50).optional(),
|
||||||
|
batch_info: z.string().trim().max(255).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateBottleData = z.infer<typeof UpdateBottleSchema>;
|
||||||
|
|
||||||
|
export const TagSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(50),
|
||||||
|
category: z.enum(['nose', 'taste', 'finish', 'texture']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TagData = z.infer<typeof TagSchema>;
|
||||||
|
|
||||||
|
export const AdminCreditUpdateSchema = z.object({
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
newBalance: z.number().min(0).max(1000000),
|
||||||
|
reason: z.string().trim().min(1).max(255),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AdminCreditUpdateData = z.infer<typeof AdminCreditUpdateSchema>;
|
||||||
|
|
||||||
|
export const AdminSettingsSchema = z.object({
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
dailyLimit: z.number().min(0).max(10000).nullable(),
|
||||||
|
googleSearchCost: z.number().min(0).max(1000).optional(),
|
||||||
|
geminiAiCost: z.number().min(0).max(1000).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AdminSettingsData = z.infer<typeof AdminSettingsSchema>;
|
||||||
|
|
||||||
|
export const DiscoveryDataSchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(255),
|
||||||
|
distillery: z.string().trim().max(255).optional(),
|
||||||
|
abv: z.number().min(0).max(100).optional(),
|
||||||
|
age: z.number().min(0).max(100).optional(),
|
||||||
|
distilled_at: z.string().trim().max(50).optional(),
|
||||||
|
bottled_at: z.string().trim().max(50).optional(),
|
||||||
|
batch_info: z.string().trim().max(255).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DiscoveryData = z.infer<typeof DiscoveryDataSchema>;
|
||||||
|
|
||||||
|
export const BuddySchema = z.object({
|
||||||
|
name: z.string().trim().min(1).max(100),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BuddyData = z.infer<typeof BuddySchema>;
|
||||||
|
|
||||||
export interface AnalysisResponse {
|
export interface AnalysisResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: BottleMetadata;
|
data?: BottleMetadata;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
@@ -25,13 +25,15 @@
|
|||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
|||||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user