feat: add recording sharing and community features
Backend Changes: - Added original_recording_id field to sexy_recordings table to track duplicates - Added indexes for original_recording_id and public fields - Implemented /sexy/community-recordings endpoint to list public shared recordings - Implemented /sexy/recordings/:id/duplicate endpoint to duplicate community recordings - Community recordings filtered by status=published AND public=true - Duplication creates a private draft copy for the current user Frontend Changes: - Added leaderboard and profile quick links to play view header - Added navigation buttons for better UX on play page - Added translations: my_profile, anonymous, load_more Database Schema: - ALTER TABLE sexy_recordings ADD COLUMN original_recording_id uuid - Created foreign key and indexes for efficient queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -929,5 +929,119 @@ export default {
|
||||
res.status(500).json({ error: error.message || "Failed to update play" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /sexy/community-recordings - List community shared recordings
|
||||
router.get("/community-recordings", async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const recordingsService = new ItemsService("sexy_recordings", {
|
||||
schema: await getSchema(),
|
||||
accountability: null, // Public endpoint, no auth required
|
||||
knex: database,
|
||||
});
|
||||
|
||||
const recordings = await recordingsService.readByQuery({
|
||||
filter: {
|
||||
status: { _eq: "published" },
|
||||
public: { _eq: true },
|
||||
},
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"description",
|
||||
"slug",
|
||||
"duration",
|
||||
"tags",
|
||||
"date_created",
|
||||
"user_created.id",
|
||||
"user_created.first_name",
|
||||
"user_created.last_name",
|
||||
"user_created.avatar",
|
||||
],
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-date_created"],
|
||||
});
|
||||
|
||||
res.json({ data: recordings });
|
||||
} catch (error: any) {
|
||||
console.error("List community recordings error:", error);
|
||||
res.status(500).json({ error: error.message || "Failed to list community recordings" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /sexy/recordings/:id/duplicate - Duplicate a community recording to current user
|
||||
router.post("/recordings/:id/duplicate", async (req, res) => {
|
||||
try {
|
||||
const accountability = req.accountability;
|
||||
if (!accountability?.user) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
|
||||
const recordingId = req.params.id;
|
||||
|
||||
// Fetch the original recording
|
||||
const schema = await getSchema();
|
||||
const recordingsService = new ItemsService("sexy_recordings", {
|
||||
schema,
|
||||
accountability: null, // Need to read any public recording
|
||||
knex: database,
|
||||
});
|
||||
|
||||
const originalRecording = await recordingsService.readOne(recordingId, {
|
||||
fields: [
|
||||
"id",
|
||||
"title",
|
||||
"description",
|
||||
"duration",
|
||||
"events",
|
||||
"device_info",
|
||||
"tags",
|
||||
"status",
|
||||
"public",
|
||||
],
|
||||
});
|
||||
|
||||
// Verify it's a published, public recording
|
||||
if (originalRecording.status !== "published" || !originalRecording.public) {
|
||||
return res.status(403).json({ error: "Recording is not publicly shared" });
|
||||
}
|
||||
|
||||
// Create duplicate with current user's accountability
|
||||
const userRecordingsService = new ItemsService("sexy_recordings", {
|
||||
schema,
|
||||
accountability,
|
||||
knex: database,
|
||||
});
|
||||
|
||||
// Generate unique slug
|
||||
const baseSlug = originalRecording.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
const timestamp = Date.now();
|
||||
const slug = `${baseSlug}-copy-${timestamp}`;
|
||||
|
||||
const duplicatedRecording = await userRecordingsService.createOne({
|
||||
title: `${originalRecording.title} (Copy)`,
|
||||
description: originalRecording.description,
|
||||
slug,
|
||||
duration: originalRecording.duration,
|
||||
events: originalRecording.events,
|
||||
device_info: originalRecording.device_info,
|
||||
tags: originalRecording.tags || [],
|
||||
status: "draft",
|
||||
public: false,
|
||||
original_recording_id: recordingId,
|
||||
});
|
||||
|
||||
res.status(201).json({ data: duplicatedRecording });
|
||||
} catch (error: any) {
|
||||
console.error("Duplicate recording error:", error);
|
||||
res.status(500).json({ error: error.message || "Failed to duplicate recording" });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,6 +20,9 @@ export default {
|
||||
open: "Open",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
my_profile: "My Profile",
|
||||
anonymous: "Anonymous",
|
||||
load_more: "Load More",
|
||||
},
|
||||
header: {
|
||||
home: "Home",
|
||||
|
||||
@@ -437,9 +437,29 @@ onMount(() => {
|
||||
>
|
||||
{$_("play.title")}
|
||||
</h1>
|
||||
<p class="text-lg text-muted-foreground mb-10">
|
||||
<p class="text-lg text-muted-foreground mb-6">
|
||||
{$_("play.description")}
|
||||
</p>
|
||||
<div class="flex justify-center gap-3 mb-10">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
href="/leaderboard"
|
||||
class="border-primary/30 hover:bg-primary/10"
|
||||
>
|
||||
<span class="icon-[ri--trophy-line] w-4 h-4 mr-2"></span>
|
||||
{$_("gamification.leaderboard")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
href="/me"
|
||||
class="border-primary/30 hover:bg-primary/10"
|
||||
>
|
||||
<span class="icon-[ri--user-line] w-4 h-4 mr-2"></span>
|
||||
{$_("common.my_profile")}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex justify-center gap-4 items-center">
|
||||
<Button
|
||||
size="lg"
|
||||
|
||||
Reference in New Issue
Block a user