feat: replace Directus with custom Node.js GraphQL backend
Removes Directus 11 and replaces it with a lean, purpose-built backend: - packages/backend/: Fastify v5 + GraphQL Yoga v5 + Pothos (code-first) with Drizzle ORM, Redis sessions (session_token cookie), argon2 auth, Nodemailer, fluent-ffmpeg, and full gamification system ported from bundle - Frontend: @directus/sdk replaced by graphql-request v7; services.ts fully rewritten with identical signatures; directus.ts now re-exports from api.ts - Cookie renamed directus_session_token → session_token - Dev proxy target updated 8055 → 4000 - compose.yml: Directus service removed, backend service added (port 4000) - Dockerfile.backend: new multi-stage image with ffmpeg - Dockerfile: bundle build step and ffmpeg removed from frontend image - data-migration.ts: one-time script to migrate all Directus/sexy_ tables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { gql } from "graphql-request";
|
||||
import { getGraphQLClient } from "$lib/api";
|
||||
|
||||
const LEADERBOARD_QUERY = gql`
|
||||
query Leaderboard($limit: Int, $offset: Int) {
|
||||
leaderboard(limit: $limit, offset: $offset) {
|
||||
user_id display_name avatar
|
||||
total_weighted_points total_raw_points
|
||||
recordings_count playbacks_count achievements_count rank
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url, locals }) => {
|
||||
// Guard: Redirect to login if not authenticated
|
||||
@@ -11,22 +23,27 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
|
||||
const limit = parseInt(url.searchParams.get("limit") || "100");
|
||||
const offset = parseInt(url.searchParams.get("offset") || "0");
|
||||
|
||||
const response = await fetch(
|
||||
`/api/sexy/gamification/leaderboard?limit=${limit}&offset=${offset}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch leaderboard");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const client = getGraphQLClient(fetch);
|
||||
const data = await client.request<{
|
||||
leaderboard: {
|
||||
user_id: string;
|
||||
display_name: string | null;
|
||||
avatar: string | null;
|
||||
total_weighted_points: number | null;
|
||||
total_raw_points: number | null;
|
||||
recordings_count: number | null;
|
||||
playbacks_count: number | null;
|
||||
achievements_count: number | null;
|
||||
rank: number;
|
||||
}[];
|
||||
}>(LEADERBOARD_QUERY, { limit, offset });
|
||||
|
||||
return {
|
||||
leaderboard: data.data || [],
|
||||
leaderboard: data.leaderboard || [],
|
||||
pagination: {
|
||||
limit,
|
||||
offset,
|
||||
hasMore: data.data?.length === limit,
|
||||
hasMore: data.leaderboard?.length === limit,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { gql } from "graphql-request";
|
||||
import { getGraphQLClient } from "$lib/api";
|
||||
|
||||
const USER_PROFILE_QUERY = gql`
|
||||
query UserProfile($id: String!) {
|
||||
userProfile(id: $id) {
|
||||
id first_name last_name email description avatar date_created
|
||||
}
|
||||
userGamification(userId: $id) {
|
||||
stats {
|
||||
user_id total_raw_points total_weighted_points
|
||||
recordings_count playbacks_count comments_count achievements_count rank
|
||||
}
|
||||
achievements {
|
||||
id code name description icon category date_unlocked progress required_count
|
||||
}
|
||||
recent_points { action points date_created recording_id }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const load: PageServerLoad = async ({ params, locals, fetch }) => {
|
||||
// Guard: Redirect to login if not authenticated
|
||||
@@ -10,38 +30,44 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => {
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
// Fetch user profile data from Directus
|
||||
const userResponse = await fetch(`/api/users/${id}?fields=id,first_name,last_name,email,description,avatar,date_created,location`);
|
||||
const client = getGraphQLClient(fetch);
|
||||
const data = await client.request<{
|
||||
userProfile: {
|
||||
id: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
email: string;
|
||||
description: string | null;
|
||||
avatar: string | null;
|
||||
date_created: string;
|
||||
} | null;
|
||||
userGamification: {
|
||||
stats: {
|
||||
user_id: string;
|
||||
total_raw_points: number | null;
|
||||
total_weighted_points: number | null;
|
||||
recordings_count: number | null;
|
||||
playbacks_count: number | null;
|
||||
comments_count: number | null;
|
||||
achievements_count: number | null;
|
||||
rank: number;
|
||||
} | null;
|
||||
achievements: unknown[];
|
||||
recent_points: unknown[];
|
||||
} | null;
|
||||
}>(USER_PROFILE_QUERY, { id });
|
||||
|
||||
if (!userResponse.ok) {
|
||||
if (!data.userProfile) {
|
||||
throw redirect(404, "/");
|
||||
}
|
||||
|
||||
const userData = await userResponse.json();
|
||||
const user = userData.data;
|
||||
|
||||
// Fetch user's comments count
|
||||
const commentsResponse = await fetch(`/api/comments?filter[user_created][_eq]=${id}&aggregate[count]=*`);
|
||||
const commentsData = await commentsResponse.json();
|
||||
const commentsCount = commentsData.data?.[0]?.count || 0;
|
||||
|
||||
// Fetch user's video likes count
|
||||
const likesResponse = await fetch(`/api/items/sexy_video_likes?filter[user_id][_eq]=${id}&aggregate[count]=*`);
|
||||
const likesData = await likesResponse.json();
|
||||
const likesCount = likesData.data?.[0]?.count || 0;
|
||||
|
||||
// Fetch gamification data
|
||||
const gamificationResponse = await fetch(`/api/sexy/gamification/user/${id}`);
|
||||
let gamification = null;
|
||||
if (gamificationResponse.ok) {
|
||||
gamification = await gamificationResponse.json();
|
||||
}
|
||||
const gamification = data.userGamification;
|
||||
|
||||
return {
|
||||
user,
|
||||
user: data.userProfile,
|
||||
stats: {
|
||||
comments_count: commentsCount,
|
||||
likes_count: likesCount,
|
||||
comments_count: gamification?.stats?.comments_count || 0,
|
||||
likes_count: 0,
|
||||
},
|
||||
gamification,
|
||||
isOwnProfile: locals.authStatus.user?.id === id,
|
||||
|
||||
Reference in New Issue
Block a user