Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First endpoint and UI. Fix prisma migration issue on vercel deploy #31

Merged
merged 15 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 414 additions & 14 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "prisma generate && next build",
"start": "next start",
"lint": "next lint",
"test": "vitest run",
Expand All @@ -25,6 +25,7 @@
"@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5",
"@prisma/client": "4.10.1",
"@tanstack/react-query": "^5.28.4",
"@testing-library/user-event": "14.5.2",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
Expand All @@ -40,6 +41,7 @@
"next": "13.5.6",
"pre-commit": "1.2.2",
"react": "18.2.0",
"react-charts": "^3.0.0-beta.57",
"react-dom": "18.2.0",
"react-icons": "4.7.1",
"react-loader-spinner": "6.1.6",
Expand Down
13 changes: 8 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "./globals.css";
import { Analytics } from "@vercel/analytics/react";
import type { Metadata } from "next";
import { ReactQueryClientProvider } from "@/components/ReactQueryProvider";

export const metadata: Metadata = {
title: "Dico | Dictionnaire Espagnol - Français en ligne",
Expand All @@ -14,14 +15,16 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="fr">
{/*
<ReactQueryClientProvider>
<html lang="fr">
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}

<body>{children}</body>
<Analytics />
</html>
<body>{children}</body>
<Analytics />
</html>
</ReactQueryClientProvider>
);
}
3 changes: 2 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { ChakraProvider } from "@chakra-ui/react";
import theme from "@/styles/theme";
import theme from "@/utils/styles/theme";

import Main from "@/components/Main";

export default function App() {
Expand Down
14 changes: 14 additions & 0 deletions src/app/stats/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";
import Header from "@/components/Header";
import Stats from "@/components/stats/Stats";

import theme from "@/utils/styles/theme";
import { ChakraProvider } from "@chakra-ui/react";
export default function HomePage() {
return (
<ChakraProvider theme={theme} cssVarsRoot="body">
<Header />
<Stats />
</ChakraProvider>
);
}
87 changes: 25 additions & 62 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,35 @@
"use client";
import usePathName from "@/hooks/usePathName";
import { HomeIcon, MoonIcon, StatsIcon, SunIcon } from "@/utils/icons/icons";
import { useColorMode } from "@chakra-ui/react";

export function useDevMode() {
return process.env.NODE_ENV === "development";
}
export default function Header() {
const { colorMode, toggleColorMode } = useColorMode();
const path = usePathName();
const devMode = useDevMode();

return (
<header className="absolute top-0 right-0 p-5 flex justify-center items-center gap-5">
<button className="text-lg" onClick={() => toggleColorMode()}>
{colorMode === "light" ? (
<svg
width="44"
height="44"
viewBox="0 0 24 24"
stroke="#2c3e50"
fill="none"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 1.992a10 10 0 1 0 9.236 13.838c.341 -.82 -.476 -1.644 -1.298 -1.31a6.5 6.5 0 0 1 -6.864 -10.787l.077 -.08c.551 -.63 .113 -1.653 -.758 -1.653h-.266l-.068 -.006l-.06 -.002z"
fill="currentColor"
/>
</svg>
) : (
<svg
width="44"
height="44"
viewBox="0 0 24 24"
stroke="#2c3e50"
fill="none"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 19a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z"
fill="currentColor"
/>
<path
d="M18.313 16.91l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 0 1 1.218 -1.567l.102 .07z"
fill="currentColor"
/>
<path
d="M7.007 16.993a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z"
fill="currentColor"
/>
<path
d="M4 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z"
fill="currentColor"
/>
<path
d="M21 11a1 1 0 0 1 .117 1.993l-.117 .007h-1a1 1 0 0 1 -.117 -1.993l.117 -.007h1z"
fill="currentColor"
/>
<path
d="M6.213 4.81l.094 .083l.7 .7a1 1 0 0 1 -1.32 1.497l-.094 -.083l-.7 -.7a1 1 0 0 1 1.217 -1.567l.102 .07z"
fill="currentColor"
/>
<path
d="M19.107 4.893a1 1 0 0 1 .083 1.32l-.083 .094l-.7 .7a1 1 0 0 1 -1.497 -1.32l.083 -.094l.7 -.7a1 1 0 0 1 1.414 0z"
fill="currentColor"
/>
<path
d="M12 2a1 1 0 0 1 .993 .883l.007 .117v1a1 1 0 0 1 -1.993 .117l-.007 -.117v-1a1 1 0 0 1 1 -1z"
fill="currentColor"
/>
<path
d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z"
fill="currentColor"
/>
</svg>
)}
{devMode && (
<a
aria-label={path === "/stats" ? "Home" : "Stat's Page"}
href={`${path === "/stats" ? "/" : "/stats"}`}
className={`text-lg ${
colorMode === "light" ? "text-black" : "text-white"
}`}
>
{path === "/stats" ? <HomeIcon /> : <StatsIcon />}
</a>
)}
<button
aria-label="Toggle color mode"
className="text-lg"
onClick={() => toggleColorMode()}
>
{colorMode === "light" ? <SunIcon /> : <MoonIcon />}
</button>
</header>
);
Expand Down
26 changes: 26 additions & 0 deletions src/components/ReactQueryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";

export const ReactQueryClientProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
44 changes: 44 additions & 0 deletions src/components/stats/Charts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import { AxisOptions, Chart } from "react-charts";

type MyDatum = { date: Date; stars: number };

export function MyChart() {
const data = [
{
label: "React Charts",
data: [
{
date: new Date(),
stars: 23467238,
},
],
},
];

const primaryAxis = React.useMemo(
(): AxisOptions<MyDatum> => ({
getValue: (datum) => datum.date,
}),
[]
);

const secondaryAxes = React.useMemo(
(): AxisOptions<MyDatum>[] => [
{
getValue: (datum) => datum.stars,
},
],
[]
);

return (
<Chart
options={{
data,
primaryAxis,
secondaryAxes,
}}
/>
);
}
14 changes: 14 additions & 0 deletions src/components/stats/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import WordsList from "./WordsList";

export default function Stats() {
return (
<main className="pt-24 mx-auto h-full w-full justify-start">
<header className="flex flex-col gap-4 gap-y-12 pb-10">
<h1 className="text-5xl">Dico en nombres</h1>
</header>
<section className="w-full flex gap-4 gap-y-8 flex-col md:flex-row px-4">
<WordsList />
</section>
</main>
);
}
61 changes: 61 additions & 0 deletions src/components/stats/WordsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import React from "react";
import { AxisOptions, Chart } from "react-charts";

async function fetchWords() {
const response = await import("@/utils/schemas/top-searches.json");
return response.default;
}

interface WordSearches {
_count: { wordId: number };
wordSource: string;
}
export default function WordsList() {
const queryClient = useQueryClient();
const query = useQuery({ queryKey: ["words"], queryFn: fetchWords });
const primaryAxis = React.useMemo(
(): AxisOptions<WordSearches> => ({
getValue: (word) => word.wordSource,
}),
[]
);

const secondaryAxes = React.useMemo(
(): AxisOptions<WordSearches>[] => [
{
getValue: (word) => word._count.wordId,
},
],
[]
);
const data = [
{
label: "Words",
data: query.data || [],
},
];
if (query.isError) {
return <div>Failed to load data</div>;
}
if (query.isLoading) {
return <div>Loading...</div>;
}
return (
<article className="w-full h-full">
<h2 className="text-2xl text-center pb-4">
Les mots les plus recherchés
</h2>
<div className="w-full h-96">
<Chart
className="w-full h-full bg-white"
options={{
data,
primaryAxis,
secondaryAxes,
}}
/>
</div>
</article>
);
}
10 changes: 10 additions & 0 deletions src/hooks/usePathName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useState, useEffect } from "react";

export default function usePathName() {
const [path, setPath] = useState<string>("/");
useEffect(() => {
const pathName = window.location.pathname;
setPath(pathName);
}, []);
return path;
}
32 changes: 32 additions & 0 deletions src/pages/api/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../utils/db";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
try {
const mostSearchedWords = await prisma.search.groupBy({
by: ["wordSource"],
_count: {
wordId: true,
},
orderBy: {
_count: {
wordId: "desc",
},
},
take: 10, // Limite les résultats aux 10 premiers mots les plus recherchés
});

res.status(200).json(mostSearchedWords);
} catch (error) {
console.error("Failed to fetch stats:", error);
res.status(500).json({ message: "Failed to fetch statistics" });
}
} else {
res.setHeader("Allow", ["GET"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
20 changes: 20 additions & 0 deletions src/pages/api/words.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../utils/db";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
const words = [
{ id: 1, word: "Bonjour", searches: 100 },
{ id: 2, word: "Chien", searches: 75 },
{ id: 3, word: "Chat", searches: 50 },
];

res.status(200).json(words);
} else {
res.setHeader("Allow", ["GET"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Loading
Loading