refactor!: remove unused resizable components and enhance efficiency in critical areas

Remove outdated `ResizablePanelGroup`, `ResizablePanel`, and `ResizableHandle` components from the codebase. Optimize API error handling with anti-spam protection for repetitive errors. Update Dockerfile for streamlined builds, improve sitemap generation in `app`, and refactor lazy loading using `React.Suspense`. Refine services to support enhanced query parameters like `author`.
This commit is contained in:
Mathis HERRIOT
2026-01-14 16:37:55 +01:00
parent 37a23390d5
commit fbc231dc9a
8 changed files with 61 additions and 114 deletions

View File

@@ -1,66 +1,45 @@
# syntax=docker.io/docker/dockerfile:1
FROM pnpm/pnpm:20-alpine AS base
FROM node:22-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@latest --activate
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* source.config.ts* next.config.* ./
RUN \
if [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f yarn.lock ]; then yarn --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
WORKDIR /usr/src/app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY backend/package.json ./backend/
COPY frontend/package.json ./frontend/
COPY documentation/package.json ./documentation/
RUN pnpm install --no-frozen-lockfile
COPY . .
# On réinstalle après COPY pour s'assurer que tous les scripts de cycle de vie et les liens sont corrects
RUN pnpm install --no-frozen-lockfile
RUN pnpm run --filter @memegoat/frontend build
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f pnpm-lock.yaml ]; then pnpm run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f yarn.lock ]; then yarn run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /usr/src/app/frontend/public ./frontend/public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/frontend/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/frontend/.next/static ./frontend/.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
# Note: server.js is created in the standalone output.
# In a monorepo, it's often inside a subdirectory matching the package name.
CMD ["node", "frontend/server.js"]

View File

@@ -24,9 +24,13 @@ export default function DashboardLayout({
{children}
{modal}
</main>
<MobileFilters />
<React.Suspense fallback={null}>
<MobileFilters />
</React.Suspense>
</div>
<SearchSidebar />
<React.Suspense fallback={null}>
<SearchSidebar />
</React.Suspense>
</SidebarInset>
</SidebarProvider>
);

View File

@@ -1,13 +1,9 @@
"use client";
import * as React from "react";
import type { Metadata } from "next";
import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service";
export const metadata: Metadata = {
title: "Nouveautés | MemeGoat",
description: "Découvrez les derniers mèmes publiés sur MemeGoat.",
};
export default function RecentPage() {
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
ContentService.getRecent(params.limit, params.offset),

View File

@@ -1,13 +1,9 @@
"use client";
import * as React from "react";
import type { Metadata } from "next";
import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service";
export const metadata: Metadata = {
title: "Tendances | MemeGoat",
description: "Découvrez les mèmes les plus populaires du moment sur MemeGoat.",
};
export default function TrendsPage() {
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
ContentService.getTrends(params.limit, params.offset),

View File

@@ -6,7 +6,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
// Pages statiques
const routes = ["", "/trends", "/recent"].map((route) => ({
const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
changeFrequency: "daily" as const,

View File

@@ -1,56 +0,0 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -8,4 +8,31 @@ const api = axios.create({
},
});
// Système anti-spam rudimentaire pour les erreurs répétitives
const errorCache = new Map<string, number>();
const SPAM_THRESHOLD_MS = 2000; // 2 secondes de silence après une erreur sur le même endpoint
api.interceptors.response.use(
(response) => {
// Nettoyer le cache d'erreur en cas de succès sur cet endpoint
const url = response.config.url || "";
errorCache.delete(url);
return response;
},
(error) => {
const url = error.config?.url || "unknown";
const now = Date.now();
const lastErrorTime = errorCache.get(url);
if (lastErrorTime && now - lastErrorTime < SPAM_THRESHOLD_MS) {
// Ignorer l'erreur si elle se produit trop rapidement (déjà signalée)
// On retourne une promesse qui ne se résout jamais ou on rejette avec une marque spéciale
return new Promise(() => {});
}
errorCache.set(url, now);
return Promise.reject(error);
}
);
export default api;

View File

@@ -9,6 +9,7 @@ export const ContentService = {
tag?: string;
category?: string;
query?: string;
author?: string;
}): Promise<PaginatedResponse<Content>> {
const { data } = await api.get<PaginatedResponse<Content>>("/contents/explore", {
params,