Skip to content

Commit 78e6d78

Browse files
committed
New Puppy Form
1 parent bbc6548 commit 78e6d78

File tree

6 files changed

+132
-79
lines changed

6 files changed

+132
-79
lines changed

app/Http/Controllers/PuppyController.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
use App\Http\Resources\PuppyResource;
66
use App\Models\Puppy;
77
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Storage;
89
use Inertia\Inertia;
910

1011
class PuppyController extends Controller
1112
{
1213

14+
// ------------------------------
15+
// Index
16+
// ------------------------------
1317
public function index(Request $request)
1418
{
1519
$search = $request->search;
@@ -22,6 +26,7 @@ public function index(Request $request)
2226
->orWhere('trait', 'like', "%{$search}%");
2327
})
2428
->with(['user', 'likedBy'])
29+
->latest()
2530
->paginate(9)
2631
->withQueryString()
2732
),
@@ -31,10 +36,46 @@ public function index(Request $request)
3136
]);
3237
}
3338

39+
// ------------------------------
40+
// Like
41+
// ------------------------------
3442
public function like(Request $request, Puppy $puppy)
3543
{
3644
sleep(1);
3745
$puppy->likedBy()->toggle($request->user()->id);
3846
return back();
3947
}
48+
49+
// ------------------------------
50+
// Store
51+
// ------------------------------
52+
public function store(Request $request)
53+
{
54+
// Validate the data
55+
$request->validate([
56+
'name' => 'required|string|max:255',
57+
'trait' => 'required|string|max:255',
58+
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:5120',
59+
]);
60+
61+
// Store the uploaded image
62+
$image_url = null;
63+
if ($request->hasFile('image')) {
64+
$path = $request->file('image')->store('puppies', 'public');
65+
if (!$path) {
66+
return back()->withErrors(['image' => 'Failed to upload image.']);
67+
}
68+
$image_url = Storage::url($path);
69+
}
70+
71+
// Create a new Puppy instance attached to the authenticated user
72+
$puppy = $request->user()->puppies()->create([
73+
'name' => $request->name,
74+
'trait' => $request->trait,
75+
'image_url' => $image_url,
76+
]);
77+
78+
// Redirect to the same page
79+
return back()->with('success', 'Puppy created successfully!');
80+
}
4081
}

app/Models/Puppy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,9 @@ public function likedBy(): BelongsToMany
1717
{
1818
return $this->belongsToMany(User::class);
1919
}
20+
protected $fillable = [
21+
'name',
22+
'trait',
23+
'image_url',
24+
];
2025
}
Lines changed: 82 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,87 @@
1-
import { Dispatch, SetStateAction } from "react";
2-
import { Puppy } from "../types";
3-
import { useFormStatus } from "react-dom";
4-
import { createPuppy } from "../queries";
5-
import { ErrorBoundary } from "react-error-boundary";
1+
import { useForm } from '@inertiajs/react';
2+
import { useRef } from 'react';
3+
import { useFormStatus } from 'react-dom';
64

7-
export function NewPuppyForm({
8-
puppies,
9-
setPuppies,
10-
}: {
11-
puppies: Puppy[];
12-
setPuppies: Dispatch<SetStateAction<Puppy[]>>;
13-
}) {
14-
return (
15-
<div className="mt-12 flex items-center justify-between bg-white p-8 shadow ring ring-black/5">
16-
<ErrorBoundary
17-
fallbackRender={({ error }) => (
18-
<pre>{JSON.stringify(error, null, 2)}</pre>
19-
)}
20-
>
21-
<form
22-
action={async (formData: FormData) => {
23-
const response = await createPuppy(formData);
24-
if (response.data) {
25-
setPuppies([...puppies, response.data]);
26-
}
27-
}}
28-
className="mt-4 flex w-full flex-col items-start gap-4"
29-
>
30-
<div className="grid w-full gap-6 md:grid-cols-3">
31-
<fieldset className="flex w-full flex-col gap-1">
32-
<label htmlFor="name">Name</label>
33-
<input
34-
required
35-
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
36-
id="name"
37-
type="text"
38-
name="name"
39-
/>
40-
</fieldset>
41-
<fieldset className="flex w-full flex-col gap-1">
42-
<label htmlFor="trait">Personality trait</label>
43-
<input
44-
required
45-
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
46-
id="trait"
47-
type="text"
48-
name="trait"
49-
/>
50-
</fieldset>
51-
<fieldset className="col-span-2 flex w-full flex-col gap-1">
52-
<label htmlFor="image_url">Profile pic</label>
53-
<input
54-
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
55-
id="image_url"
56-
type="file"
57-
name="image_url"
58-
/>
59-
</fieldset>
60-
</div>
61-
<SubmitButton />
62-
</form>
63-
</ErrorBoundary>
64-
</div>
65-
);
5+
export function NewPuppyForm() {
6+
const { post, setData, data, errors, reset } = useForm({
7+
name: '',
8+
trait: '',
9+
image: null as File | null,
10+
});
11+
const fileInputRef = useRef<HTMLInputElement>(null);
12+
13+
return (
14+
<>
15+
<div className="mt-12 flex items-center justify-between bg-white p-8 shadow ring ring-black/5">
16+
<form
17+
onSubmit={(e) => {
18+
e.preventDefault();
19+
post(route('puppies.store'), {
20+
preserveScroll: true,
21+
onSuccess: () => {
22+
reset();
23+
if (fileInputRef.current) {
24+
fileInputRef.current.value = '';
25+
}
26+
},
27+
});
28+
}}
29+
className="mt-4 flex w-full flex-col items-start gap-4"
30+
>
31+
<div className="grid w-full gap-6 md:grid-cols-3">
32+
<fieldset className="flex w-full flex-col gap-1">
33+
<label htmlFor="name">Name</label>
34+
<input
35+
value={data.name}
36+
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
37+
id="name"
38+
type="text"
39+
name="name"
40+
onChange={(e) => setData('name', e.target.value)}
41+
/>
42+
{errors.name && <p className="mt-1 text-xs text-red-500">{errors.name}</p>}
43+
</fieldset>
44+
<fieldset className="flex w-full flex-col gap-1">
45+
<label htmlFor="trait">Personality trait</label>
46+
<input
47+
value={data.trait}
48+
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
49+
id="trait"
50+
type="text"
51+
name="trait"
52+
onChange={(e) => setData('trait', e.target.value)}
53+
/>
54+
{errors.trait && <p className="mt-1 text-xs text-red-500">{errors.trait}</p>}
55+
</fieldset>
56+
<fieldset className="col-span-2 flex w-full flex-col gap-1">
57+
<label htmlFor="image">Profile pic</label>
58+
<input
59+
ref={fileInputRef}
60+
className="max-w-96 rounded-sm bg-white px-2 py-1 ring ring-black/20 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
61+
id="image"
62+
type="file"
63+
name="image"
64+
onChange={(e) => setData('image', e.target.files ? e.target.files[0] : null)}
65+
/>
66+
{errors.image && <p className="mt-1 text-xs text-red-500">{errors.image}</p>}
67+
</fieldset>
68+
</div>
69+
<SubmitButton />
70+
</form>
71+
</div>
72+
</>
73+
);
6674
}
6775

6876
function SubmitButton() {
69-
const status = useFormStatus();
70-
return (
71-
<button
72-
className="mt-4 inline-block rounded bg-cyan-300 px-4 py-2 font-medium text-cyan-900 hover:bg-cyan-200 focus:ring-2 focus:ring-cyan-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-slate-200"
73-
type="submit"
74-
disabled={status.pending}
75-
>
76-
{status.pending
77-
? `Adding ${status?.data?.get("name") || "puppy"}...`
78-
: "Add puppy"}
79-
</button>
80-
);
77+
const status = useFormStatus();
78+
return (
79+
<button
80+
className="mt-4 inline-block rounded bg-cyan-300 px-4 py-2 font-medium text-cyan-900 hover:bg-cyan-200 focus:ring-2 focus:ring-cyan-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-slate-200"
81+
type="submit"
82+
disabled={status.pending}
83+
>
84+
{status.pending ? `Adding ${status?.data?.get('name') || 'puppy'}...` : 'Add puppy'}
85+
</button>
86+
);
8187
}

resources/js/components/Shortlist.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useForm, usePage } from '@inertiajs/react';
22
import { Heart, LoaderCircle, X } from 'lucide-react';
33
import { Puppy, SharedData } from '../types';
44

5+
// TODO: Make sure all the liked puppies are showing, not just the ones from the current page.
56
export function Shortlist({ puppies }: { puppies: Puppy[] }) {
67
const { auth } = usePage<SharedData>().props;
78
return (

resources/js/pages/puppies/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Shortlist } from '@/components/Shortlist';
88

99
import { Filters, PaginatedResponse, Puppy, SharedData } from '@/types';
1010
import { usePage } from '@inertiajs/react';
11-
import { useState } from 'react';
1211

1312
export default function App({ puppies, filters }: { puppies: PaginatedResponse<Puppy>; filters: Filters }) {
1413
return (
@@ -22,7 +21,6 @@ export default function App({ puppies, filters }: { puppies: PaginatedResponse<P
2221
}
2322

2423
function Main({ paginatedPuppies, filters }: { paginatedPuppies: PaginatedResponse<Puppy>; filters: Filters }) {
25-
const [puppies, setPuppies] = useState<Puppy[]>(paginatedPuppies.data);
2624
const { auth } = usePage<SharedData>().props;
2725

2826
return (
@@ -32,7 +30,7 @@ function Main({ paginatedPuppies, filters }: { paginatedPuppies: PaginatedRespon
3230
{auth.user && <Shortlist puppies={paginatedPuppies.data} />}
3331
</div>
3432
<PuppiesList puppies={paginatedPuppies} />
35-
<NewPuppyForm puppies={paginatedPuppies.data} setPuppies={setPuppies} />
33+
{auth.user && <NewPuppyForm />}
3634
</main>
3735
);
3836
}

routes/web.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
Route::middleware(['auth', 'verified'])->group(function () {
1111
Route::patch('puppies/{puppy}/like', [PuppyController::class, 'like'])
1212
->name('puppies.like');
13+
Route::post('puppies', [PuppyController::class, 'store'])
14+
->name('puppies.store');
1315

1416
Route::get('dashboard', function () {
1517
return Inertia::render('dashboard');

0 commit comments

Comments
 (0)