feat: add new item creation form with Supabase integration

This commit is contained in:
Greg 2025-05-18 21:32:22 +02:00
parent 64f7e3b6f3
commit e0bc98d320
3 changed files with 189 additions and 5 deletions

View File

@ -7,4 +7,15 @@ The tech stack for MyFavStuff should be based on:
- Authentication: basic_auth with Caddy (reverse proxy) to get started with (You shouldn't do anything about this yet)
# Coding pattern preferences
- Always prefer simple solutions
- Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality
- Write code that takes into account the different environments: dev, test and prod
- You are careful to only make changes that are requested or you are confident that all changes are well understood and related to the change being requested
- When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don't have duplicate logic.
- Keep the codebase very clean and organized
- Avoid writing scripts in files if possible, especially if the script is likely only to be run once
- Avoid having files over 200-300 lines of code. Refactor at that point
- Mocking data is only needed for test, never mock data for dev or prod
- Never add stubbing or fake data patterns to code that affects the dev or prod environments
- Never overwrite my .env file without first asking and confirming

View File

@ -0,0 +1,50 @@
// app/add-item/actions.js
'use server';
import { supabase } from '@/lib/supabaseClient'; // Assuming alias @ is configured for root
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createItem(formData) {
if (!supabase) {
return { error: 'Supabase client is not initialized. Cannot create item.' };
}
const newItem = {
title: formData.get('title'),
type: formData.get('type'),
rating: formData.get('rating') ? parseInt(formData.get('rating'), 10) : null,
notes: formData.get('notes'),
image_url: formData.get('image_url'),
// created_at will be set by default in Supabase or can be added here if needed
};
// Basic validation
if (!newItem.title || !newItem.type) {
return { error: 'Title and Type are required.' };
}
if (newItem.rating !== null && (newItem.rating < 1 || newItem.rating > 5)) {
return { error: 'Rating must be between 1 and 5.' };
}
try {
const { data, error } = await supabase
.from('items')
.insert([newItem])
.select(); // .select() to get the inserted data back
if (error) {
console.error('Supabase insert error:', error);
return { error: `Failed to create item: ${error.message}` };
}
console.log('Item created successfully:', data);
revalidatePath('/'); // Revalidate the homepage to show the new item
// No explicit redirect here, form can handle success message
return { success: true, message: 'Item added successfully!', createdItem: data ? data[0] : null };
} catch (e) {
console.error('Error in createItem action:', e);
return { error: 'An unexpected error occurred.' };
}
}

View File

@ -1,11 +1,134 @@
// app/add-item/page.js
'use client';
import { useState, useTransition } from 'react';
import { createItem } from './actions'; // Server Action
import { useRouter } from 'next/navigation';
export default function AddItemPage() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState(null);
const [successMessage, setSuccessMessage] = useState('');
const handleSubmit = async (event) => {
event.preventDefault();
setError(null);
setSuccessMessage('');
const formData = new FormData(event.currentTarget);
startTransition(async () => {
const result = await createItem(formData);
if (result.error) {
setError(result.error);
} else if (result.success) {
setSuccessMessage(result.message);
// Optionally clear the form or redirect
event.target.reset(); // Clear form fields
// router.push('/'); // Or redirect to homepage after a delay
setTimeout(() => router.push('/'), 1500); // Redirect after 1.5s
}
});
};
return (
<div>
<h1>Add New Item</h1>
<p>This is where the form to add a new item will go.</p>
{/* You can add your form component or elements here */}
<div className="max-w-2xl mx-auto p-4 sm:p-6 lg:p-8 text-neutral-100">
<header className="mb-8">
<h1 className="text-3xl font-bold text-sky-400">Add New Favorite Item</h1>
<p className="text-neutral-400 mt-1">
Fill in the details below to add a new item to your collection.
</p>
</header>
{error && (
<div className="mb-4 p-3 bg-red-900 border border-red-700 text-red-100 rounded">
<p className="font-semibold">Error:</p>
<p>{error}</p>
</div>
)}
{successMessage && (
<div className="mb-4 p-3 bg-green-900 border border-green-700 text-green-100 rounded">
<p className="font-semibold">Success:</p>
<p>{successMessage}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6 bg-neutral-800 p-6 rounded-lg shadow-xl">
<div>
<label htmlFor="title" className="block text-sm font-medium text-neutral-300 mb-1">Title <span className="text-red-500">*</span></label>
<input
type="text"
name="title"
id="title"
required
className="block w-full bg-neutral-700 border-neutral-600 rounded-md shadow-sm py-2 px-3 text-neutral-100 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="e.g., The Hitchhiker's Guide to the Galaxy"
/>
</div>
<div>
<label htmlFor="type" className="block text-sm font-medium text-neutral-300 mb-1">Type <span className="text-red-500">*</span></label>
<select
name="type"
id="type"
required
className="block w-full bg-neutral-700 border-neutral-600 rounded-md shadow-sm py-2 px-3 text-neutral-100 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
>
<option value="">Select a type</option>
<option value="book">Book</option>
<option value="movie">Movie</option>
<option value="series">TV Series</option>
<option value="game">Game</option>
<option value="music">Music Album</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label htmlFor="rating" className="block text-sm font-medium text-neutral-300 mb-1">Rating (1-5)</label>
<input
type="number"
name="rating"
id="rating"
min="1"
max="5"
className="block w-full bg-neutral-700 border-neutral-600 rounded-md shadow-sm py-2 px-3 text-neutral-100 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="e.g., 5"
/>
</div>
<div>
<label htmlFor="image_url" className="block text-sm font-medium text-neutral-300 mb-1">Image URL</label>
<input
type="url"
name="image_url"
id="image_url"
className="block w-full bg-neutral-700 border-neutral-600 rounded-md shadow-sm py-2 px-3 text-neutral-100 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="https://example.com/image.jpg"
/>
</div>
<div>
<label htmlFor="notes" className="block text-sm font-medium text-neutral-300 mb-1">Notes</label>
<textarea
name="notes"
id="notes"
rows="4"
className="block w-full bg-neutral-700 border-neutral-600 rounded-md shadow-sm py-2 px-3 text-neutral-100 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
placeholder="Why is this item your favorite? Any interesting thoughts?"
></textarea>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={isPending}
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-sky-600 hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800 focus:ring-sky-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPending ? 'Adding Item...' : 'Add Item'}
</button>
</div>
</form>
</div>
);
}