From ce85d6240271b5a3886a3a8ef698bcbc468044ad Mon Sep 17 00:00:00 2001 From: greg Date: Sun, 27 Apr 2025 20:24:22 +0200 Subject: [PATCH] 1st try --- my-favorites-app/public/placeholder-book.jpg | 1 + my-favorites-app/src/app/admin/add/page.tsx | 122 ++++++++++++++++++ my-favorites-app/src/app/admin/page.tsx | 81 ++++++++++++ my-favorites-app/src/app/globals.css | 60 ++++++--- my-favorites-app/src/app/layout.tsx | 23 +++- my-favorites-app/src/app/page.tsx | 111 ++-------------- .../src/components/FavoriteCard.tsx | 48 +++++++ .../src/components/FavoritesGrid.tsx | 24 ++++ my-favorites-app/src/components/Header.tsx | 23 ++++ my-favorites-app/src/components/ui/button.tsx | 53 ++++++++ my-favorites-app/src/components/ui/card.tsx | 79 ++++++++++++ my-favorites-app/src/lib/supabase.ts | 105 +++++++++++++++ my-favorites-app/src/lib/utils.ts | 33 +++++ 13 files changed, 646 insertions(+), 117 deletions(-) create mode 100644 my-favorites-app/public/placeholder-book.jpg create mode 100644 my-favorites-app/src/app/admin/add/page.tsx create mode 100644 my-favorites-app/src/app/admin/page.tsx create mode 100644 my-favorites-app/src/components/FavoriteCard.tsx create mode 100644 my-favorites-app/src/components/FavoritesGrid.tsx create mode 100644 my-favorites-app/src/components/Header.tsx create mode 100644 my-favorites-app/src/components/ui/button.tsx create mode 100644 my-favorites-app/src/components/ui/card.tsx create mode 100644 my-favorites-app/src/lib/supabase.ts create mode 100644 my-favorites-app/src/lib/utils.ts diff --git a/my-favorites-app/public/placeholder-book.jpg b/my-favorites-app/public/placeholder-book.jpg new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/my-favorites-app/public/placeholder-book.jpg @@ -0,0 +1 @@ + diff --git a/my-favorites-app/src/app/admin/add/page.tsx b/my-favorites-app/src/app/admin/add/page.tsx new file mode 100644 index 0000000..6e6b7db --- /dev/null +++ b/my-favorites-app/src/app/admin/add/page.tsx @@ -0,0 +1,122 @@ +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; + +export default function AddFavoritePage() { + return ( +
+
+

Add New Favorite

+ + + +
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +

+ Upload a cover image or provide a URL below +

+
+ +
+ + +
+
+ +
+ +
+
+
+ +
+

+ Note: This form is a placeholder. In the actual implementation, it would submit data to Supabase. +

+
+
+ ); +} diff --git a/my-favorites-app/src/app/admin/page.tsx b/my-favorites-app/src/app/admin/page.tsx new file mode 100644 index 0000000..12e738b --- /dev/null +++ b/my-favorites-app/src/app/admin/page.tsx @@ -0,0 +1,81 @@ +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; +import { getFavorites } from '@/lib/supabase'; + +export default async function AdminPage() { + const favorites = await getFavorites(); + + return ( +
+
+

Admin Dashboard

+

+ Manage your favorite items here. Authentication is handled by Caddy with basic_auth. +

+
+ +
+
+

Manage Favorites

+ + + +
+ +
+ + + + + + + + + + + + {favorites.length === 0 ? ( + + + + ) : ( + favorites.map((favorite) => ( + + + + + + + + )) + )} + +
TitleAuthorTypeDateActions
+ No favorites found. Add your first one! +
{favorite.title}{favorite.author}{favorite.type}{new Date(favorite.consumption_date).toLocaleDateString()} +
+ + + + +
+
+
+
+ +
+ + + +
+ +
+

+ Note: This admin area would be protected by Caddy's basic_auth in production. +

+
+
+ ); +} diff --git a/my-favorites-app/src/app/globals.css b/my-favorites-app/src/app/globals.css index a2dc41e..f1d1f6b 100644 --- a/my-favorites-app/src/app/globals.css +++ b/my-favorites-app/src/app/globals.css @@ -1,26 +1,56 @@ -@import "tailwindcss"; +@tailwind base; +@tailwind components; +@tailwind utilities; :root { - --background: #ffffff; + --background: #f5f5f0; --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --card: #ffffff; + --card-foreground: #171717; + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #f3f4f6; + --secondary-foreground: #111827; + --muted: #f3f4f6; + --muted-foreground: #6b7280; + --accent: #f3f4f6; + --accent-foreground: #111827; + --destructive: #ef4444; + --destructive-foreground: #ffffff; + --border: #e5e7eb; + --input: #e5e7eb; + --ring: #3b82f6; + --radius: 0.5rem; } @media (prefers-color-scheme: dark) { :root { - --background: #0a0a0a; - --foreground: #ededed; + --background: #0c0a09; + --foreground: #fafaf9; + --card: #1c1917; + --card-foreground: #fafaf9; + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #27272a; + --secondary-foreground: #fafafa; + --muted: #27272a; + --muted-foreground: #a1a1aa; + --accent: #27272a; + --accent-foreground: #fafafa; + --destructive: #7f1d1d; + --destructive-foreground: #fef2f2; + --border: #27272a; + --input: #27272a; + --ring: #3b82f6; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } } diff --git a/my-favorites-app/src/app/layout.tsx b/my-favorites-app/src/app/layout.tsx index f7fa87e..e196398 100644 --- a/my-favorites-app/src/app/layout.tsx +++ b/my-favorites-app/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; +import { Header } from "@/components/Header"; import "./globals.css"; const geistSans = Geist({ @@ -13,8 +14,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "My Favorites", + description: "A collection of my favorite books, movies, series, and more", }; export default function RootLayout({ @@ -25,9 +26,23 @@ export default function RootLayout({ return ( - {children} +
+
+
+
+ {children} +
+
+
+
+

+ © {new Date().getFullYear()} My Favorites. All rights reserved. +

+
+
+
); diff --git a/my-favorites-app/src/app/page.tsx b/my-favorites-app/src/app/page.tsx index e68abe6..fea99b7 100644 --- a/my-favorites-app/src/app/page.tsx +++ b/my-favorites-app/src/app/page.tsx @@ -1,103 +1,18 @@ -import Image from "next/image"; +import { FavoritesGrid } from '@/components/FavoritesGrid'; +import { getFavorites } from '@/lib/supabase'; -export default function Home() { +export default async function Home() { + const favorites = await getFavorites(); + return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- +
+
+

My Favorites

+

+ A collection of books, movies, series, and more that have made an impression on me. +

+
+
); } diff --git a/my-favorites-app/src/components/FavoriteCard.tsx b/my-favorites-app/src/components/FavoriteCard.tsx new file mode 100644 index 0000000..c64f5fb --- /dev/null +++ b/my-favorites-app/src/components/FavoriteCard.tsx @@ -0,0 +1,48 @@ +import Image from 'next/image'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Favorite } from '@/lib/supabase'; +import { formatDate, getTypeIcon } from '@/lib/utils'; + +interface FavoriteCardProps { + favorite: Favorite; +} + +export function FavoriteCard({ favorite }: FavoriteCardProps) { + return ( + +
+ {favorite.cover_image ? ( + {`Cover { + // Handle image loading error by setting a fallback + const target = e.target as HTMLImageElement; + target.src = `/placeholder-${favorite.type}.jpg`; + target.onerror = null; // Prevent infinite loop + }} + /> + ) : ( +
+ {getTypeIcon(favorite.type)} +
+ )} +
+ {favorite.type} +
+
+ + {favorite.title} + + +

{favorite.author}

+
+ + {formatDate(favorite.consumption_date)} + +
+ ); +} diff --git a/my-favorites-app/src/components/FavoritesGrid.tsx b/my-favorites-app/src/components/FavoritesGrid.tsx new file mode 100644 index 0000000..8a9c9ce --- /dev/null +++ b/my-favorites-app/src/components/FavoritesGrid.tsx @@ -0,0 +1,24 @@ +import { Favorite } from '@/lib/supabase'; +import { FavoriteCard } from './FavoriteCard'; + +interface FavoritesGridProps { + favorites: Favorite[]; +} + +export function FavoritesGrid({ favorites }: FavoritesGridProps) { + if (!favorites || favorites.length === 0) { + return ( +
+

No favorites found. Check back later!

+
+ ); + } + + return ( +
+ {favorites.map((favorite) => ( + + ))} +
+ ); +} diff --git a/my-favorites-app/src/components/Header.tsx b/my-favorites-app/src/components/Header.tsx new file mode 100644 index 0000000..0bfcfff --- /dev/null +++ b/my-favorites-app/src/components/Header.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link'; +import { Button } from './ui/button'; + +export function Header() { + return ( +
+
+
+ + My Favorites + +
+
+ +
+
+
+ ); +} diff --git a/my-favorites-app/src/components/ui/button.tsx b/my-favorites-app/src/components/ui/button.tsx new file mode 100644 index 0000000..8798dba --- /dev/null +++ b/my-favorites-app/src/components/ui/button.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/my-favorites-app/src/components/ui/card.tsx b/my-favorites-app/src/components/ui/card.tsx new file mode 100644 index 0000000..8cce8b6 --- /dev/null +++ b/my-favorites-app/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/my-favorites-app/src/lib/supabase.ts b/my-favorites-app/src/lib/supabase.ts new file mode 100644 index 0000000..a1d77e0 --- /dev/null +++ b/my-favorites-app/src/lib/supabase.ts @@ -0,0 +1,105 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''; +const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''; + +export const supabase = createClient(supabaseUrl, supabaseKey); + +export type Favorite = { + id: string; + title: string; + author: string; + consumption_date: string; + type: 'book' | 'movie' | 'series' | 'concert' | 'exhibition' | 'comicbook' | 'other'; + cover_image: string; + created_at: string; + updated_at: string; +}; + +// Utility functions for working with favorites + +export async function getFavorites(): Promise { + try { + const { data, error } = await supabase + .from('favorites') + .select('*') + .order('consumption_date', { ascending: false }); + + if (error) throw error; + return data as Favorite[]; + } catch (error) { + console.error('Error fetching favorites:', error); + return []; + } +} + +export async function addFavorite(favorite: Omit): Promise { + try { + const { data, error } = await supabase + .from('favorites') + .insert(favorite) + .select() + .single(); + + if (error) throw error; + return data as Favorite; + } catch (error) { + console.error('Error adding favorite:', error); + return null; + } +} + +export async function updateFavorite(id: string, favorite: Partial>): Promise { + try { + const { data, error } = await supabase + .from('favorites') + .update(favorite) + .eq('id', id) + .select() + .single(); + + if (error) throw error; + return data as Favorite; + } catch (error) { + console.error('Error updating favorite:', error); + return null; + } +} + +export async function deleteFavorite(id: string): Promise { + try { + const { error } = await supabase + .from('favorites') + .delete() + .eq('id', id); + + if (error) throw error; + return true; + } catch (error) { + console.error('Error deleting favorite:', error); + return false; + } +} + +export async function uploadCoverImage(file: File, path: string): Promise { + try { + const { data, error } = await supabase.storage + .from('covers') + .upload(path, file, { + cacheControl: '3600', + upsert: true + }); + + if (error) throw error; + + // Get the public URL for the uploaded file + const { data: { publicUrl } } = supabase.storage + .from('covers') + .getPublicUrl(data.path); + + return publicUrl; + } catch (error) { + console.error('Error uploading cover image:', error); + return null; + } +} diff --git a/my-favorites-app/src/lib/utils.ts b/my-favorites-app/src/lib/utils.ts new file mode 100644 index 0000000..ce73715 --- /dev/null +++ b/my-favorites-app/src/lib/utils.ts @@ -0,0 +1,33 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long' + }); +} + +export function getTypeIcon(type: string): string { + switch (type) { + case 'book': + return '📚'; + case 'movie': + return '🎬'; + case 'series': + return '📺'; + case 'concert': + return '🎵'; + case 'exhibition': + return '🖼️'; + case 'comicbook': + return '💬'; + default: + return '🔖'; + } +}