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:
@@ -1,66 +1,45 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1
|
# 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
|
FROM base AS builder
|
||||||
WORKDIR /app
|
WORKDIR /usr/src/app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
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 . .
|
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.
|
FROM node:22-alpine AS runner
|
||||||
# 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
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
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 addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
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
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/frontend/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/frontend/.next/static ./frontend/.next/static
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV PORT=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"
|
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"]
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ export default function DashboardLayout({
|
|||||||
{children}
|
{children}
|
||||||
{modal}
|
{modal}
|
||||||
</main>
|
</main>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
<MobileFilters />
|
<MobileFilters />
|
||||||
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
<React.Suspense fallback={null}>
|
||||||
<SearchSidebar />
|
<SearchSidebar />
|
||||||
|
</React.Suspense>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { ContentList } from "@/components/content-list";
|
import { ContentList } from "@/components/content-list";
|
||||||
import { ContentService } from "@/services/content.service";
|
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() {
|
export default function RecentPage() {
|
||||||
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
||||||
ContentService.getRecent(params.limit, params.offset),
|
ContentService.getRecent(params.limit, params.offset),
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { Metadata } from "next";
|
|
||||||
import { ContentList } from "@/components/content-list";
|
import { ContentList } from "@/components/content-list";
|
||||||
import { ContentService } from "@/services/content.service";
|
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() {
|
export default function TrendsPage() {
|
||||||
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
const fetchFn = React.useCallback((params: { limit: number; offset: number }) =>
|
||||||
ContentService.getTrends(params.limit, params.offset),
|
ContentService.getTrends(params.limit, params.offset),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
|
||||||
|
|
||||||
// Pages statiques
|
// Pages statiques
|
||||||
const routes = ["", "/trends", "/recent"].map((route) => ({
|
const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map((route) => ({
|
||||||
url: `${baseUrl}${route}`,
|
url: `${baseUrl}${route}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "daily" as const,
|
changeFrequency: "daily" as const,
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
@@ -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;
|
export default api;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const ContentService = {
|
|||||||
tag?: string;
|
tag?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
author?: string;
|
||||||
}): Promise<PaginatedResponse<Content>> {
|
}): Promise<PaginatedResponse<Content>> {
|
||||||
const { data } = await api.get<PaginatedResponse<Content>>("/contents/explore", {
|
const { data } = await api.get<PaginatedResponse<Content>>("/contents/explore", {
|
||||||
params,
|
params,
|
||||||
|
|||||||
Reference in New Issue
Block a user