Skip to content

Commit 684d0fd

Browse files
committed
Last push
1 parent 99ecfd7 commit 684d0fd

19 files changed

+1132
-124
lines changed

chelle-fulk-main/api/recordings.php

Lines changed: 751 additions & 35 deletions
Large diffs are not rendered by default.

chelle-fulk-main/api/seed_videos.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
title VARCHAR(255) NOT NULL,
2525
thumbnail VARCHAR(500) NOT NULL,
2626
embed_id VARCHAR(50) NOT NULL UNIQUE,
27-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
27+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
28+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
2829
)";
2930

3031
if (!$conn->query($createTableSQL)) {

chelle-fulk-main/api/videos.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function checkAuth() {
142142
switch ($method) {
143143
case 'GET':
144144
// Get all videos (public access)
145-
$result = $conn->query("SELECT id, title, thumbnail, embed_id FROM chellefulk_main_video ORDER BY id ASC");
145+
$result = $conn->query("SELECT id, title, thumbnail, embed_id FROM chellefulk_main_video ORDER BY created_at DESC");
146146
$videos = [];
147147
while ($row = $result->fetch_assoc()) {
148148
$videos[] = [

chelle-fulk-main/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function App() {
3939
<Route path="/contact" element={<ContactForm />} />
4040
<Route path="/videos" element={<Videos />} />
4141
<Route path="/recordings" element={<Recordings />} />
42-
<Route path="dbtest" element={<DbConnectionTest />} />
42+
<Route path="/dbtest" element={<DbConnectionTest />} />
4343

4444
<Route path="/admin" element={<AdminPage />} />
4545

chelle-fulk-main/src/components/DbConnectionTest.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ const DbConnectionTest: React.FC = () => {
1717

1818
try {
1919
const response = await testDatabaseConnection();
20-
console.log('Details are:', response);
2120
setResult({
2221
success: response.success,
2322
message: response.message,

chelle-fulk-main/src/components/forms/AdminCredentialForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ const AdminCredentialForm: React.FC = () => {
3737
role: response.user?.role,
3838
expiresAt: expiresAt
3939
});
40-
console.log("Login successful. Token expires at:", new Date(expiresAt).toLocaleTimeString());
4140
setStatus({ success: true, message: "Login successful!" });
4241
} catch (err: any) {
4342
setStatus({ success: false, message: err.message || "Unknown error" });

chelle-fulk-main/src/components/forms/RecordingForm.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,13 @@ const RecordingForm: React.FC<RecordingFormProps> = ({ onClose, onCancel, form,
127127
// Validation function
128128
const validate = (data: RecordingDTO): ValidationErrors => {
129129
const errs: ValidationErrors = {};
130-
// Image file required
131-
if (!imageFile) {
132-
errs.image = "Image file is required.";
130+
131+
// Skip file validation in edit mode
132+
if (!editMode) {
133+
// Image file required (only for new recordings)
134+
if (!imageFile) {
135+
errs.image = "Image file is required.";
136+
}
133137
}
134138

135139
// Title required
@@ -155,15 +159,18 @@ const RecordingForm: React.FC<RecordingFormProps> = ({ onClose, onCancel, form,
155159
errs.performers = performerErrors;
156160
}
157161

158-
// Validate samples: trackName and audio file required
159-
const sampleErrors = data.samples.map((sample, i) => {
160-
const sErr: { trackName?: string; audioUrl?: string } = {};
161-
if (!sample.trackName.trim()) sErr.trackName = "Track name is required.";
162-
if (!audioFiles[i]) sErr.audioUrl = "Audio file is required.";
163-
return sErr;
164-
});
165-
if (sampleErrors.some((e) => Object.keys(e).length > 0)) {
166-
errs.samples = sampleErrors;
162+
// Validate samples: trackName required, audio file only required for new recordings
163+
// Skip sample validation entirely in edit mode
164+
if (!editMode) {
165+
const sampleErrors = data.samples.map((sample, i) => {
166+
const sErr: { trackName?: string; audioUrl?: string } = {};
167+
if (!sample.trackName.trim()) sErr.trackName = "Track name is required.";
168+
if (!audioFiles[i]) sErr.audioUrl = "Audio file is required.";
169+
return sErr;
170+
});
171+
if (sampleErrors.some((e) => Object.keys(e).length > 0)) {
172+
errs.samples = sampleErrors;
173+
}
167174
}
168175

169176
return errs;
@@ -199,7 +206,9 @@ const RecordingForm: React.FC<RecordingFormProps> = ({ onClose, onCancel, form,
199206
formData.append("link", form.link);
200207
// Send performers and samples as JSON strings for backend compatibility
201208
formData.append("performers", JSON.stringify(form.performers));
202-
formData.append("samples", JSON.stringify(form.samples));
209+
// Filter out empty samples before sending
210+
const validSamples = form.samples.filter(s => s.trackName.trim() !== '');
211+
formData.append("samples", JSON.stringify(validSamples));
203212
// Image file
204213
if (imageFile) {
205214
formData.append("image", imageFile);
@@ -222,6 +231,7 @@ const RecordingForm: React.FC<RecordingFormProps> = ({ onClose, onCancel, form,
222231
dispatch({ type: "RESET" });
223232
onClose();
224233
} catch (error: any) {
234+
console.error('Form submission error:', error);
225235
setStatus({ success: false, message: error.message || (editMode ? "Failed to update recording." : "Failed to submit recording.") });
226236
} finally {
227237
setLoading(false);

chelle-fulk-main/src/components/forms/VideoForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ const VideoForm: React.FC<VideoFormProps> = ({ onClose, onCancel, form, setForm,
5858
}
5959
setLoading(true);
6060
try {
61-
console.log(editMode ? 'Updating' : 'Submitting', 'with token:', credentials?.token);
6261
if (editMode) {
6362
await updateVideo(form.id, form, credentials?.token);
6463
setStatus({ success: true, message: "Your video has been successfully updated." });

chelle-fulk-main/src/components/pages/Recordings.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ const Recordings: React.FC = () => {
5151
const [playingId, setPlayingId] = useState<string | null>(null);
5252
const audioRefs = useRef<{ [id: string]: HTMLAudioElement | null }>({});
5353

54+
// Handle sample deletion
55+
const handleSampleDeleted = (recordingId: number, sampleId: number) => {
56+
setRecordings(recordings =>
57+
recordings.map(rec => {
58+
if (rec.id === recordingId) {
59+
return {
60+
...rec,
61+
samples: rec.samples.filter(s => s.id !== sampleId)
62+
};
63+
}
64+
return rec;
65+
})
66+
);
67+
};
68+
5469
return (
5570
<>
5671
<PaddingWrapper mdPadding="md:pt-12 md:p-8">
@@ -134,17 +149,7 @@ const Recordings: React.FC = () => {
134149
dispatch({ type: 'SET_PERFORMER', index: idx, value: performer });
135150
}
136151
});
137-
// Set samples
138-
recording.samples.forEach((sample, idx) => {
139-
if (idx === 0) {
140-
dispatch({ type: 'SET_SAMPLE_FIELD', index: 0, field: 'trackName', value: sample.trackName });
141-
dispatch({ type: 'SET_SAMPLE_FIELD', index: 0, field: 'audioUrl', value: sample.audioUrl });
142-
} else {
143-
dispatch({ type: 'ADD_SAMPLE' });
144-
dispatch({ type: 'SET_SAMPLE_FIELD', index: idx, field: 'trackName', value: sample.trackName });
145-
dispatch({ type: 'SET_SAMPLE_FIELD', index: idx, field: 'audioUrl', value: sample.audioUrl });
146-
}
147-
});
152+
// Samples are not pre-populated in edit mode
148153
setErrors({});
149154
setIsModalOpen(true);
150155
}}
@@ -191,6 +196,10 @@ const Recordings: React.FC = () => {
191196
playingId={playingId}
192197
setPlayingId={setPlayingId}
193198
audioRefs={audioRefs}
199+
isAdmin={!!credentials}
200+
adminToken={credentials?.token}
201+
deleteProtectionEnabled={deleteProtectionEnabled}
202+
onSampleDeleted={handleSampleDeleted}
194203
/>
195204
</div>
196205
);
@@ -205,6 +214,10 @@ const Recordings: React.FC = () => {
205214
playingId={playingId}
206215
setPlayingId={setPlayingId}
207216
audioRefs={audioRefs}
217+
isAdmin={!!credentials}
218+
adminToken={credentials?.token}
219+
deleteProtectionEnabled={deleteProtectionEnabled}
220+
onSampleDeleted={handleSampleDeleted}
208221
/>
209222
</div>
210223
);

chelle-fulk-main/src/components/recordings/AudioSamples.tsx

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
import React from 'react';
2-
import { PlayIcon, PauseIcon } from '@heroicons/react/24/solid';
1+
import React, { useState } from 'react';
2+
import { PlayIcon, PauseIcon, TrashIcon } from '@heroicons/react/24/solid';
33
import { SampleDTO } from '../../models/RecordingsDTO';
4+
import { deleteSample } from '../../services/apis/recordingService';
45
import './AudioSamples.scss';
56

67
type Props = {
78
samples: SampleDTO[];
89
albumId: string;
10+
recordingId: number;
911
playingId: string | null;
1012
setPlayingId: (id: string | null) => void;
1113
audioRefs: React.MutableRefObject<{ [id: string]: HTMLAudioElement | null }>;
14+
isAdmin?: boolean;
15+
adminToken?: string;
16+
deleteProtectionEnabled?: boolean;
17+
onSampleDeleted?: (sampleId: number) => void;
1218
};
1319

1420
const formatTime = (seconds: number) => {
@@ -17,9 +23,12 @@ const formatTime = (seconds: number) => {
1723
return `${mins}:${secs.toString().padStart(2, '0')}`;
1824
};
1925

20-
const AudioSamples: React.FC<Props> = ({ samples, albumId, playingId, setPlayingId, audioRefs }) => {
26+
const AudioSamples: React.FC<Props> = ({ samples, albumId, recordingId, playingId, setPlayingId, audioRefs, isAdmin, adminToken, deleteProtectionEnabled = true, onSampleDeleted }) => {
27+
const [sampleToDelete, setSampleToDelete] = useState<SampleDTO | null>(null);
28+
const [deleting, setDeleting] = useState(false);
29+
const [deleteError, setDeleteError] = useState<string | null>(null);
30+
2131
if (samples.length === 0) return null;
22-
const limitedSamples = samples.slice(0, 4);
2332

2433
const handlePlayPause = (index: number) => {
2534
const id = `${albumId}-${index}`;
@@ -40,9 +49,30 @@ const AudioSamples: React.FC<Props> = ({ samples, albumId, playingId, setPlaying
4049
if (playingId === id) setPlayingId(null);
4150
};
4251

52+
const handleDeleteSample = async (sample: SampleDTO) => {
53+
if (!sample.id) return;
54+
setDeleting(true);
55+
setDeleteError(null);
56+
try {
57+
await deleteSample(recordingId, sample.id, adminToken);
58+
onSampleDeleted?.(sample.id);
59+
setSampleToDelete(null);
60+
} catch (err: any) {
61+
setDeleteError('Failed to delete sample: ' + (err.message || 'Unknown error'));
62+
} finally {
63+
setDeleting(false);
64+
}
65+
};
66+
4367
return (
68+
<>
4469
<div className="scroll-container space-y-2">
45-
{limitedSamples.map((sample, index) => {
70+
{deleteError && (
71+
<div className="bg-red-700 text-white rounded px-3 py-2 text-sm mb-2">
72+
{deleteError}
73+
</div>
74+
)}
75+
{samples.map((sample, index) => {
4676
const id = `${albumId}-${index}`;
4777
return (
4878
<div key={id} className="flex items-center gap-3 p-2 bg-black/20 rounded-md hover:bg-black/30 transition-colors">
@@ -79,10 +109,54 @@ const AudioSamples: React.FC<Props> = ({ samples, albumId, playingId, setPlaying
79109
) : (
80110
<div className="text-xs text-neutral-300">Sample unavailable</div>
81111
)}
112+
{isAdmin && sample.id && (
113+
<button
114+
onClick={async (e) => {
115+
e.stopPropagation();
116+
if (deleteProtectionEnabled) {
117+
setSampleToDelete(sample);
118+
} else {
119+
await handleDeleteSample(sample);
120+
}
121+
}}
122+
className="flex items-center justify-center w-6 h-6 hover:bg-yellow-600/30 rounded transition-colors flex-shrink-0"
123+
aria-label="Delete sample"
124+
disabled={deleting}
125+
>
126+
<TrashIcon className="h-4 w-4 text-yellow-400 hover:text-yellow-300" />
127+
</button>
128+
)}
82129
</div>
83130
);
84131
})}
85132
</div>
133+
134+
{/* Delete Confirmation Modal */}
135+
{deleteProtectionEnabled && sampleToDelete && (
136+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70">
137+
<div className="bg-gray-900 rounded-lg shadow-lg p-8 max-w-md w-full text-center border border-yellow-400">
138+
<h2 className="text-xl text-yellow-300 mb-4 font-semibold">Confirm Delete</h2>
139+
<p className="text-white mb-6">Are you sure you want to delete sample <span className="font-bold">{sampleToDelete.trackName}</span>?</p>
140+
<div className="flex justify-center gap-6">
141+
<button
142+
className="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded font-bold"
143+
disabled={deleting}
144+
onClick={() => handleDeleteSample(sampleToDelete)}
145+
>
146+
{deleting ? 'Deleting...' : 'Delete'}
147+
</button>
148+
<button
149+
className="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded font-bold"
150+
onClick={() => setSampleToDelete(null)}
151+
disabled={deleting}
152+
>
153+
Cancel
154+
</button>
155+
</div>
156+
</div>
157+
</div>
158+
)}
159+
</>
86160
);
87161
};
88162

0 commit comments

Comments
 (0)