Skip to content

Commit d8e4272

Browse files
Merge pull request #14 from TigerAppsOrg/type_switch
Image edit page
2 parents 817f3ea + e3f7d59 commit d8e4272

File tree

7 files changed

+199
-35
lines changed

7 files changed

+199
-35
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,28 @@ pnpm db:migrate
3434
pnpm dev
3535
```
3636

37-
The frontend runs on `http://localhost:5173` and the API on `http://localhost:3001`.
37+
The frontend runs on `http://localhost:5173` and the API on `http://localhost:4000`.
3838

3939
### Environment Variables
4040

4141
**Server** (`apps/server/.env`):
4242

4343
```env
44-
PORT=3001
44+
PORT=4000
4545
NODE_ENV=development
4646
DATABASE_URL=postgresql://spot:spot_password@localhost:5433/spot_db
4747
JWT_SECRET=your-secret-at-least-32-chars
4848
CLOUDINARY_CLOUD_NAME=your-cloud-name
4949
CLOUDINARY_API_KEY=your-api-key
5050
CLOUDINARY_API_SECRET=your-api-secret
5151
FRONTEND_URL=http://localhost:5173
52-
CAS_SERVICE_URL=http://localhost:3001/api/auth/callback
52+
CAS_SERVICE_URL=http://localhost:4000/api/auth/callback
5353
```
5454

5555
**Frontend** (`apps/frontend/.env`):
5656

5757
```env
58-
VITE_API_URL=http://localhost:3001
58+
VITE_API_URL=http://localhost:4000
5959
```
6060

6161
## Scripts

apps/frontend/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# API URL for backend server
2-
VITE_API_URL=http://localhost:3001
2+
VITE_API_URL=http://localhost:4000

apps/frontend/src/lib/api/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ const getApiBaseUrl = () => {
1010
}
1111
// Dev fallback: use same host as frontend with backend port
1212
if (typeof window !== 'undefined') {
13-
return `http://${window.location.hostname}:3001`;
13+
return `http://${window.location.hostname}:4000`;
1414
}
15-
return 'http://localhost:3001';
15+
return 'http://localhost:4000';
1616
};
1717

1818
const API_BASE_URL = getApiBaseUrl();
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<script lang="ts">
2+
import Card from './Card.svelte';
3+
import Button from './Button.svelte';
4+
import Map from './Map.svelte';
5+
import { updateImage, type Picture } from '$lib/api/admin';
6+
7+
interface Props {
8+
image: Picture;
9+
onClose: () => void;
10+
onSave: (updated: Picture) => void;
11+
}
12+
13+
let { image, onClose, onSave }: Props = $props();
14+
15+
// Local state for editable fields
16+
let editCoordinates = $state({ lat: image.latitude, lng: image.longitude });
17+
let editDifficulty = $state<'EASY' | 'MEDIUM' | 'HARD'>(image.difficulty);
18+
let editShowInDaily = $state(image.showInDaily);
19+
let editShowInVersus = $state(image.showInVersus);
20+
let editShowInTournament = $state(image.showInTournament);
21+
let saving = $state(false);
22+
23+
const difficultyOptions = [
24+
{ value: 'EASY', label: 'Easy', color: 'bg-lime' },
25+
{ value: 'MEDIUM', label: 'Medium', color: 'bg-cyan' },
26+
{ value: 'HARD', label: 'Hard', color: 'bg-magenta' }
27+
];
28+
29+
function handleMapSelect(coords: { lat: number; lng: number }) {
30+
editCoordinates = coords;
31+
}
32+
33+
async function handleSave() {
34+
saving = true;
35+
try {
36+
const updated = await updateImage(image.id, {
37+
latitude: editCoordinates.lat,
38+
longitude: editCoordinates.lng,
39+
difficulty: editDifficulty,
40+
showInDaily: editShowInDaily,
41+
showInVersus: editShowInVersus,
42+
showInTournament: editShowInTournament
43+
});
44+
if (updated) {
45+
onSave(updated);
46+
} else {
47+
alert('Failed to update image');
48+
}
49+
} catch (error) {
50+
alert('Failed to update image');
51+
}
52+
saving = false;
53+
}
54+
</script>
55+
56+
<!-- Fullscreen Edit Modal -->
57+
<div
58+
class="fixed inset-0 bg-primary z-50 flex flex-col"
59+
role="dialog"
60+
tabindex="-1"
61+
onkeydown={(e) => e.key === 'Escape' && onClose()}
62+
>
63+
<!-- Header -->
64+
<div class="p-5 flex items-center justify-between bg-white flex-shrink-0 brutal-border-b">
65+
<h2 class="text-xl font-black uppercase">Edit Image</h2>
66+
<button
67+
type="button"
68+
onclick={onClose}
69+
class="w-10 h-10 bg-magenta text-white brutal-border font-black text-lg hover:bg-magenta/80 transition-colors flex items-center justify-center"
70+
title="Close"
71+
>
72+
×
73+
</button>
74+
</div>
75+
76+
<!-- Content -->
77+
<div class="flex-1 overflow-auto p-6">
78+
<div class="w-full h-full max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
79+
<!-- Image Panel (Left) -->
80+
<div class="lg:w-1/2 flex flex-col min-h-[300px] lg:min-h-0">
81+
<Card class="flex flex-col p-0 overflow-hidden h-full">
82+
<div class="p-5">
83+
<h3 class="text-lg font-black uppercase">Image Preview</h3>
84+
</div>
85+
<div class="relative grow bg-gray w-full block">
86+
<img src={image.imageUrl} alt="Location" class="w-full h-full object-cover" />
87+
</div>
88+
</Card>
89+
</div>
90+
91+
<!-- Map Panel (Right) -->
92+
<div class="lg:w-1/2 flex flex-col min-h-[300px] lg:min-h-0">
93+
<Card class="flex flex-col p-0 overflow-hidden h-full">
94+
<div class="p-5">
95+
<h3 class="text-lg font-black uppercase">Location (click to change)</h3>
96+
<p class="text-xs font-mono opacity-60 mt-1">
97+
{editCoordinates.lat.toFixed(6)}, {editCoordinates.lng.toFixed(6)}
98+
</p>
99+
</div>
100+
<div class="grow">
101+
<Map onSelect={handleMapSelect} guessLocation={editCoordinates} centerOnGuess={true} />
102+
</div>
103+
</Card>
104+
</div>
105+
</div>
106+
107+
<!-- Settings Section -->
108+
<div class="max-w-7xl mx-auto mt-6">
109+
<Card>
110+
<div class="grid md:grid-cols-2 gap-6">
111+
<!-- Difficulty -->
112+
<div>
113+
<label class="block text-sm font-bold uppercase mb-2">Difficulty</label>
114+
<div class="grid grid-cols-3 gap-2">
115+
{#each difficultyOptions as option}
116+
<button
117+
type="button"
118+
onclick={() => (editDifficulty = option.value as 'EASY' | 'MEDIUM' | 'HARD')}
119+
class="brutal-border px-4 py-3 font-bold text-sm uppercase transition-all {editDifficulty ===
120+
option.value
121+
? `${option.color} text-white`
122+
: 'bg-white hover:bg-gray/50'}"
123+
>
124+
{option.label}
125+
</button>
126+
{/each}
127+
</div>
128+
</div>
129+
130+
<!-- Mode Visibility -->
131+
<div>
132+
<label class="block text-sm font-bold uppercase mb-2">Show In Game Modes</label>
133+
<div class="flex flex-wrap gap-4">
134+
<label class="flex items-center gap-2 cursor-pointer">
135+
<input
136+
type="checkbox"
137+
bind:checked={editShowInDaily}
138+
class="w-5 h-5 brutal-border accent-orange"
139+
/>
140+
<span class="font-bold">Daily</span>
141+
</label>
142+
<label class="flex items-center gap-2 cursor-pointer">
143+
<input
144+
type="checkbox"
145+
bind:checked={editShowInVersus}
146+
class="w-5 h-5 brutal-border accent-cyan"
147+
/>
148+
<span class="font-bold">Versus</span>
149+
</label>
150+
<label class="flex items-center gap-2 cursor-pointer">
151+
<input
152+
type="checkbox"
153+
bind:checked={editShowInTournament}
154+
class="w-5 h-5 brutal-border accent-magenta"
155+
/>
156+
<span class="font-bold">Tournament</span>
157+
</label>
158+
</div>
159+
</div>
160+
</div>
161+
</Card>
162+
</div>
163+
</div>
164+
165+
<!-- Footer -->
166+
<div class="p-5 bg-white flex-shrink-0 brutal-border-t flex justify-end gap-3">
167+
<Button variant="magenta" onclick={onClose} disabled={saving}>Cancel</Button>
168+
<Button variant="cyan" onclick={handleSave} disabled={saving}>
169+
{saving ? 'Saving...' : 'Save Changes'}
170+
</Button>
171+
</div>
172+
</div>

apps/frontend/src/routes/admin/images/+page.svelte

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import Card from '$lib/components/Card.svelte';
66
import ImageUpload from '$lib/components/ImageUpload.svelte';
77
import Map from '$lib/components/Map.svelte';
8-
import GamePreviewModal from '$lib/components/GamePreviewModal.svelte';
9-
import { listImages, uploadImage, deleteImage, type Picture } from '$lib/api/admin';
8+
import ImageEditModal from '$lib/components/ImageEditModal.svelte';
9+
import { listImages, uploadImage, deleteImage, updateImage, type Picture } from '$lib/api/admin';
1010
import { userStore } from '$lib/stores/user.svelte';
1111
1212
// Form state
@@ -26,8 +26,7 @@
2626
2727
// Images from API
2828
let images = $state<Picture[]>([]);
29-
let previewImage = $state<string | null>(null);
30-
let previewCoords = $state<{ lat: number; lng: number } | null>(null);
29+
let editingImage = $state<Picture | null>(null);
3130
3231
const difficultyOptions = [
3332
{ value: 'EASY', label: 'Easy', color: 'bg-lime' },
@@ -134,14 +133,17 @@
134133
}
135134
}
136135
137-
function showPreview(imageUrl: string, coords: { lat: number; lng: number }) {
138-
previewImage = imageUrl;
139-
previewCoords = coords;
136+
function openEdit(image: Picture) {
137+
editingImage = image;
140138
}
141139
142-
function closePreview() {
143-
previewImage = null;
144-
previewCoords = null;
140+
function closeEdit() {
141+
editingImage = null;
142+
}
143+
144+
function handleEditSave(updatedImage: Picture) {
145+
images = images.map((img) => (img.id === updatedImage.id ? updatedImage : img));
146+
editingImage = null;
145147
}
146148
</script>
147149

@@ -319,17 +321,7 @@
319321
{/if}
320322
</div>
321323
<div class="flex gap-2 flex-wrap">
322-
<Button
323-
variant="cyan"
324-
onclick={() =>
325-
showPreview(image.imageUrl, {
326-
lat: image.latitude,
327-
lng: image.longitude
328-
})}
329-
class="flex-1"
330-
>
331-
Preview
332-
</Button>
324+
<Button variant="cyan" onclick={() => openEdit(image)} class="flex-1">Edit</Button>
333325
<Button variant="magenta" onclick={() => handleDelete(image.id)} class="flex-1">
334326
Delete
335327
</Button>
@@ -341,7 +333,7 @@
341333
{/if}
342334
</div>
343335

344-
<!-- Preview Modal -->
345-
{#if previewImage && previewCoords}
346-
<GamePreviewModal imageUrl={previewImage} coords={previewCoords} onClose={closePreview} />
336+
<!-- Edit Modal -->
337+
{#if editingImage}
338+
<ImageEditModal image={editingImage} onClose={closeEdit} onSave={handleEditSave} />
347339
{/if}

apps/server/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Server
2-
PORT=3001
2+
PORT=4000
33
NODE_ENV=development
44

55
# Database
@@ -17,4 +17,4 @@ CLOUDINARY_API_SECRET=your-api-secret
1717
FRONTEND_URL=http://localhost:5173
1818

1919
# CAS Authentication
20-
CAS_SERVICE_URL=http://localhost:3001/api/auth/callback
20+
CAS_SERVICE_URL=http://localhost:4000/api/auth/callback

apps/server/src/config/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import dotenv from 'dotenv';
33
dotenv.config();
44

55
export const config = {
6-
port: parseInt(process.env.PORT || '3001', 10),
6+
port: parseInt(process.env.PORT || '4000', 10),
77
nodeEnv: process.env.NODE_ENV || 'development',
88

99
database: {
@@ -27,6 +27,6 @@ export const config = {
2727

2828
cas: {
2929
baseUrl: 'https://fed.princeton.edu/cas',
30-
serviceUrl: process.env.CAS_SERVICE_URL || 'http://localhost:3001/api/auth/callback'
30+
serviceUrl: process.env.CAS_SERVICE_URL || 'http://localhost:4000/api/auth/callback'
3131
}
3232
} as const;

0 commit comments

Comments
 (0)