diff --git a/.dockerignore b/.dockerignore index b909791..cfabe27 100644 --- a/.dockerignore +++ b/.dockerignore @@ -32,4 +32,7 @@ Dockerfile* .gitignore # docs -README.md \ No newline at end of file +README.md + +# data +/data diff --git a/.env.example b/.env.example index adfe836..2985555 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,27 @@ # When adding additional environment variables, the schema in "/src/env.js" # should be updated accordingly. -# Example: -# SERVERVAR="foo" -# NEXT_PUBLIC_CLIENTVAR="bar" +# General +NODE_ENV="development" +NEXT_PUBLIC_SITE_URL="http://localhost:3000" + +# Database +DATABASE_HOST="localhost" +DATABASE_PORT="5432" +DATABASE_USER="user" +DATABASE_PASSWORD="password" +DATABASE_NAME="database" + +# Storage +STORAGE_HOST="localhost" +STORAGE_PORT="9000" +STORAGE_USER="user" +STORAGE_PASSWORD="password" +STORAGE_NAME="images" + +# Auth +FEIDE_CLIENT_ID= +FEIDE_CLIENT_SECRET= +FEIDE_AUTHORIZATION_ENDPOINT="https://auth.dataporten.no/oauth/authorization" +FEIDE_TOKEN_ENDPOINT="https://auth.dataporten.no/oauth/token" +FEIDE_USERINFO_ENDPOINT="https://auth.dataporten.no/openid/userinfo" diff --git a/.gitignore b/.gitignore index 4752b1d..c55f229 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ next-env.d.ts # site map public/sitemap.xml -public/robots.txt \ No newline at end of file +public/robots.txt + +# data +/data diff --git a/Dockerfile b/Dockerfile index deda379..45c64fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,14 @@ WORKDIR /app COPY package.json bun.lockb ./ -RUN bun install --production +RUN bun install --production --frozen-lockfile COPY . . +ENV SKIP_ENV_VALIDATION=true ENV NODE_ENV=production RUN bun run build EXPOSE 3000 -ENTRYPOINT [ "bun", "run", "start" ] \ No newline at end of file +ENTRYPOINT [ "bun", "run", "start" ] diff --git a/README.md b/README.md index 3d62a9c..11fbb20 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,40 @@ Here is a list of documentation to help you get started: +### Frontend + - [React](https://react.dev/reference/react) - Library for building user interfaces - [Next.js](https://nextjs.org/docs) - Framework for routing and server-side rendering - [Next-intl](https://next-intl-docs.vercel.app/) - Internationalization library +- [nuqs](https://nuqs.47ng.com/docs/installation) - Easy to use query params +- [BlockNote](https://www.blocknotejs.org/docs) - Tool for markdown textboxes +- [Tanstack Form](https://tanstack.com/form/latest/docs/overview) - When we need to handle form validation (shadcn/ui uses react-hook-form. but I think this is better, we will figure it out) +- [Tanstack Query](https://tanstack.com/query/latest/docs/framework/react/overview) - TRPC wraps Tanstack Query which is how we fetch data from the backend + +#### Styling + - [Tailwind CSS](https://tailwindcss.com/docs) - Styling library - [Class Variance Authority](https://beta.cva.style/) - Tool for creating style variants in our UI components -- [Shadcn/ui](https://ui.shadcn.com/docs) - Reusable UI components - - [Radix UI Primitives](https://www.radix-ui.com/primitives/docs/overview/introduction) - Primitives library that Shadcn/ui is built on, great documentation if you need to access the underlying components +- [shadcn/ui](https://ui.shadcn.com/docs) - Reusable UI components + - [Radix UI Primitives](https://www.radix-ui.com/primitives/docs/overview/introduction) - Primitives library that shadcn/ui is built on, great documentation if you need to access the underlying components +- [Aceternity/ui](https://ui.aceternity.com/components) - More fancy components that can be used (matches shadcn/ui) +- [tsparticles](https://github.com/tsparticles/react) - Cool particles library we can use as backgrounds - [Lucide](https://lucide.dev/icons/) - Icons library +### Backend + +- [TRPC](https://trpc.io/docs) - Tool for creating API endpoints as functions +- [Lucia](https://lucia-auth.com) - Authentication library +- [Drizzle](https://orm.drizzle.team/docs/overview) - ORM for interacting with the database (Postgres under the hood) +- [s3-client](https://github.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) - AWS S3 client for uploading files + +### Infrastructure + +- [Docker](https://docs.docker.com/get-started/) - Containerization tool for the application, database and storage +- [Colima](https://github.com/abiosoft/colima) - Container runtime for docker, I recommend this over Docker Desktop because of performance and license +- [Docker Compose](https://docs.docker.com/compose/) - Tool for running multi-container applications +- [nginx](https://nginx.org/en/docs/) - Reverse proxy for routing requests to the correct service + ### Other resources - [Mozilla](https://developer.mozilla.org/en-US/) - Great resource for looking up documentation for web technologies @@ -24,8 +49,8 @@ Here is a list of documentation to help you get started: - When you want to link to a new internal page use the `` component from `@/lib/navigation` instead of the normal anchortag ``. This will ensure that the page is loaded with the correct locale. If you want to link to external resources or other media, use the built-in `` component from Next.js. Remember to add `prefetch={false}` to the `` component if the page is not visited often. - If you need to use both `` components from `@/lib/navigation` and Next.js, make sure to import the Next.js `` component as `ExternalLink` to avoid naming conflicts. -- Remember to surround Links with the `Button` ui component. This will provide some basic styling and accessibility features for keyboard navigation even if it is not supposed to look like a button. -- For interationalization use the `useTranslations` hook from `next-intl`. For client components you can pass the translations as props. +- Remember to surround Links with the `Button` UI component. This will provide some basic styling and accessibility features for keyboard navigation even if it is not supposed to look like a button. +- For internationalization use the `useTranslations` hook from `next-intl`. For client components you can pass the translations as props. ## Development setup @@ -49,7 +74,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the ## Build -When you build the project, you prerender all the Server Side Generated (SSG) pages. This makes the site load faster and perform better and behave like it will when it is deployed. When serving the built project it will not hot reload when you make changes to the code like it does in development mode. +When you build the project, you pre-render all the Server Side Generated (SSG) pages. This makes the site load faster and perform better and behave like it will when it is deployed. When serving the built project it will not hot reload when you make changes to the code like it does in development mode. You can build the project with the following command: diff --git a/bun.lockb b/bun.lockb index 523264d..ebf6923 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..50e0d20 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + app: + depends_on: + - db + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + db: + image: postgres:16 + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + volumes: + - ./data/db:/var/lib/postgresql/data + ports: + - "5432:5432" + s3: + image: bitnami/minio:2024 + environment: + MINIO_ROOT_USER: ${STORAGE_USER} + MINIO_ROOT_PASSWORD: ${STORAGE_PASSWORD} + MINIO_DEFAULT_BUCKETS: ${STORAGE_NAME} + volumes: + - ./data/s3:/bitnami/minio/data + ports: + - "9000:9000" diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..c39240d --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { env } from '@/env'; +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/server/db/schema/*.ts', + dialect: 'postgresql', + dbCredentials: { + url: `postgresql://${env.DATABASE_USER}:${env.DATABASE_PASSWORD}@${env.DATABASE_HOST}:${env.DATABASE_PORT}/${env.DATABASE_NAME}`, + }, +}); diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 0894518..3a6914b 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,6 +1,8 @@ +import { env } from '@/env'; + /** @type {import('next-sitemap').IConfig} */ const config = { - siteUrl: process.env.SITE_URL ?? 'https://localhost:3000', + siteUrl: env.NEXT_PUBLIC_SITE_URL, generateRobotsTxt: true, generateIndexSitemap: false, }; diff --git a/next.config.js b/next.config.js index bb92a5b..26cdcc5 100644 --- a/next.config.js +++ b/next.config.js @@ -5,7 +5,7 @@ import nextIntl from 'next-intl/plugin'; * for Docker builds. */ await import('./src/env.js'); -const withNextIntl = nextIntl('./src/i18n.ts'); +const withNextIntl = nextIntl('./src/lib/locale/i18n.ts'); /** @type {import("next").NextConfig} */ const config = {}; diff --git a/package.json b/package.json index ff97bbe..61d917a 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,17 @@ "build": "next build", "dev": "next dev --turbo", "lint": "biome check --write --unsafe", - "start": "next start" + "start": "next start", + "db:start": "docker-compose up db", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "s3:start": "docker-compose up s3" }, "dependencies": { - "@libsql/client": "^0.6.2", + "@aws-sdk/client-s3": "^3.637.0", + "@lucia-auth/adapter-drizzle": "^1.1.0", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", @@ -23,26 +30,30 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.1", "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^5.45.1", - "@trpc/client": "^10.45.2", - "@trpc/react-query": "^10.45.2", - "@trpc/server": "^10.45.2", + "@tanstack/react-query": "^5.53.1", + "@trpc/client": "^11.0.0-rc.490", + "@trpc/react-query": "^11.0.0-rc.490", + "@trpc/server": "^11.0.0-rc.490", "autoprefixer": "^10.4.19", + "client-only": "^0.0.1", "cmdk": "1.0.0", "country-flag-icons": "^1.5.12", "cva": "^1.0.0-beta.1", - "drizzle-orm": "^0.31.2", - "lucide-react": "^0.429.0", + "drizzle-orm": "^0.33.0", + "lucia": "^3.2.0", + "lucide-react": "^0.396.0", "next": "^14.2.4", - "next-intl": "^3.15.2", + "next-intl": "^3.18.1", "next-sitemap": "^4.2.3", "next-themes": "^0.3.0", "nuqs": "^1.17.4", + "postgres": "^3.4.4", "react": "^18.3.1", "react-dom": "^18.3.1", "reading-time": "^1.5.0", "server-only": "^0.0.1", "sharp": "^0.33.4", + "superjson": "^2.2.1", "tailwind-merge": "^2.5.2", "zod": "^3.23.8" }, @@ -51,7 +62,7 @@ "@types/node": "^20.14.8", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "drizzle-kit": "^0.22.7", + "drizzle-kit": "^0.24.1", "lefthook": "^1.7.14", "postcss": "^8.4.38", "tailwind-scrollbar": "^3.1.0", diff --git a/src/app/[locale]/(default)/layout.tsx b/src/app/[locale]/(default)/layout.tsx index ac6416d..c01ad96 100644 --- a/src/app/[locale]/(default)/layout.tsx +++ b/src/app/[locale]/(default)/layout.tsx @@ -1,8 +1,7 @@ -import { unstable_setRequestLocale } from 'next-intl/server'; - import { Footer } from '@/components/layout/Footer'; import { Header } from '@/components/layout/Header'; import { Main } from '@/components/layout/Main'; +import { unstable_setRequestLocale } from 'next-intl/server'; type DefaultLayoutProps = { children: React.ReactNode; diff --git a/src/app/[locale]/(default)/news/(header)/layout.tsx b/src/app/[locale]/(default)/news/(header)/layout.tsx index eca5470..feebab9 100644 --- a/src/app/[locale]/(default)/news/(header)/layout.tsx +++ b/src/app/[locale]/(default)/news/(header)/layout.tsx @@ -1,8 +1,8 @@ -import { SquarePen } from 'lucide-react'; +import { SquarePenIcon } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { unstable_setRequestLocale } from 'next-intl/server'; -import { Link } from '@/lib/navigation'; +import { Link } from '@/lib/locale/navigation'; import { Button } from '@/components/ui/Button'; @@ -23,7 +23,7 @@ export default function NewsHeaderLayout({

{t('title')}

diff --git a/src/app/[locale]/(default)/page.tsx b/src/app/[locale]/(default)/page.tsx index 4d0817f..af70a75 100644 --- a/src/app/[locale]/(default)/page.tsx +++ b/src/app/[locale]/(default)/page.tsx @@ -1,14 +1,17 @@ +import { HelloWorld } from '@/components/home/HelloWorld'; +import { api } from '@/lib/api/server'; import { unstable_setRequestLocale } from 'next-intl/server'; -export default function HomePage({ +export default async function HomePage({ params: { locale }, }: { params: { locale: string }; }) { unstable_setRequestLocale(locale); + const hello = await api.test.helloWorld(); return (
-

Landing Page

+

{hello}

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Magni delectus cupiditate debitis! Fuga minus quod ea eligendi exercitationem. Sequi @@ -44,6 +47,7 @@ export default function HomePage({ assumenda sequi dolorem asperiores quidem quis eos quaerat dolor. Excepturi, aspernatur suscipit.

+
); } diff --git a/src/app/[locale]/(default)/storage/layout.tsx b/src/app/[locale]/(default)/storage/layout.tsx index c941e8f..0a81eae 100644 --- a/src/app/[locale]/(default)/storage/layout.tsx +++ b/src/app/[locale]/(default)/storage/layout.tsx @@ -1,14 +1,12 @@ -import { useTranslations } from 'next-intl'; - import { Button } from '@/components/ui/Button'; - import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/Tooltip'; -import { ShoppingCart } from 'lucide-react'; +import { ShoppingCartIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; export default function StorageLayout({ children, @@ -25,7 +23,7 @@ export default function StorageLayout({ diff --git a/src/app/[locale]/(default)/storage/page.tsx b/src/app/[locale]/(default)/storage/page.tsx index 26128df..223f604 100644 --- a/src/app/[locale]/(default)/storage/page.tsx +++ b/src/app/[locale]/(default)/storage/page.tsx @@ -1,14 +1,7 @@ -import { items } from '@/mock-data/items'; -import { useTranslations } from 'next-intl'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; -import Image from 'next/image'; -import { createSearchParamsCache, parseAsInteger } from 'nuqs/parsers'; - import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; import { Button } from '@/components/ui/Button'; import { Card, - CardContent, CardDescription, CardFooter, CardHeader, @@ -23,6 +16,11 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/Select'; +import { items } from '@/mock-data/items'; +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import Image from 'next/image'; +import { createSearchParamsCache, parseAsInteger } from 'nuqs/server'; export async function generateMetadata({ params: { locale }, diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 24bfeeb..fdfa2c5 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,11 +1,8 @@ +import { RootProviders } from '@/components/providers/RootProviders'; +import { routing } from '@/lib/locale'; +import { cx } from '@/lib/utils'; import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; import { Inter, Montserrat } from 'next/font/google'; -import { notFound } from 'next/navigation'; - -import { locales } from '@/lib/config'; -import { cx } from '@/lib/utils'; - -import { RootProviders } from '@/components/providers/RootProviders'; type LocaleLayoutProps = { children: React.ReactNode; @@ -25,7 +22,7 @@ const montserrat = Montserrat({ }); export function generateStaticParams() { - return locales.map((locale) => ({ locale })); + return routing.locales.map((locale) => ({ locale })); } export async function generateMetadata({ @@ -75,7 +72,6 @@ export default function LocaleLayout({ children, params: { locale }, }: LocaleLayoutProps) { - if (!locales.includes(locale)) notFound(); unstable_setRequestLocale(locale); return ( { + return createTRPCContext({ + headers: request.headers, + }); +}; + +const handler = (request: NextRequest) => + fetchRequestHandler({ + endpoint: '/api/data', + req: request, + router: appRouter, + createContext: () => createContext(request), + onError: + env.NODE_ENV === 'development' + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ''}: ${error.message}`, + ); + } + : undefined, + }); + +export { handler as GET, handler as POST }; diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 9d1c1de..3f80952 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,10 +1,9 @@ 'use client'; +import { routing } from '@/lib/locale'; import { redirect, usePathname } from 'next/navigation'; -import { defaultLocale } from '@/lib/config'; - export default function NotFound() { const pathname = usePathname(); - redirect(`/${defaultLocale}${pathname}`); + redirect(`/${routing.defaultLocale}/${pathname}`); } diff --git a/src/components/assets/icons/Facebook.tsx b/src/components/assets/icons/FacebookIcon.tsx similarity index 81% rename from src/components/assets/icons/Facebook.tsx rename to src/components/assets/icons/FacebookIcon.tsx index dd534e3..5b7aa7b 100644 --- a/src/components/assets/icons/Facebook.tsx +++ b/src/components/assets/icons/FacebookIcon.tsx @@ -1,4 +1,4 @@ -function Facebook({ className, ...rest }: { className?: string }) { +function FacebookIcon({ className, ...rest }: { className?: string }) { return ( diff --git a/src/components/assets/sponsors/KiDLogo.tsx b/src/components/assets/sponsors/KiDLogo.tsx deleted file mode 100644 index 1ce2cb6..0000000 --- a/src/components/assets/sponsors/KiDLogo.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { cx } from '@/lib/utils'; - -function KiDLogo({ - className, - title, - ...rest -}: { - className?: string; - title: string; -}) { - return ( - - - - - - - - - - - - - - - - - - ); -} - -export { KiDLogo }; diff --git a/src/components/assets/sponsors/NexusLogo.tsx b/src/components/assets/sponsors/NexusLogo.tsx new file mode 100644 index 0000000..90945b6 --- /dev/null +++ b/src/components/assets/sponsors/NexusLogo.tsx @@ -0,0 +1,187 @@ +import { cx } from '@/lib/utils'; + +function NexusLogo({ + className, + title, + ...rest +}: { + className?: string; + title: string; +}) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export { NexusLogo }; diff --git a/src/components/assets/sponsors/index.tsx b/src/components/assets/sponsors/index.tsx new file mode 100644 index 0000000..f7388d2 --- /dev/null +++ b/src/components/assets/sponsors/index.tsx @@ -0,0 +1,2 @@ +export * from './IDILogo'; +export * from './NexusLogo'; diff --git a/src/components/home/HelloWorld.tsx b/src/components/home/HelloWorld.tsx new file mode 100644 index 0000000..4be51a8 --- /dev/null +++ b/src/components/home/HelloWorld.tsx @@ -0,0 +1,11 @@ +'use client'; +import { api } from '@/lib/api/client'; + +function HelloWorld() { + // Grunnleggende eksempel på get request i en client-side komponent + // Se på react query info fra hvordan dette fungerer + const { data: hello, isLoading } = api.test.helloWorld.useQuery(); + return

{isLoading ? 'laster inn test' : hello}

; +} + +export { HelloWorld }; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index b2dd899..899d8b0 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,18 +1,17 @@ -import { Bug, Mail } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import ExternalLink from 'next/link'; - -import { Link } from '@/lib/navigation'; - -import { Facebook } from '@/components/assets/icons/Facebook'; -import { Github } from '@/components/assets/icons/Github'; -import { Instagram } from '@/components/assets/icons/Instagram'; -import { Slack } from '@/components/assets/icons/Slack'; -import { IDILogo } from '@/components/assets/sponsors/IDILogo'; -import { KiDLogo } from '@/components/assets/sponsors/KiDLogo'; +import { + FacebookIcon, + GitHubIcon, + InstagramIcon, + SlackIcon, +} from '@/components/assets/icons'; +import { IDILogo, NexusLogo } from '@/components/assets/sponsors'; import { LogoLink } from '@/components/layout/LogoLink'; import { Nav } from '@/components/layout/Nav'; import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { BugIcon, MailIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import ExternalLink from 'next/link'; function Footer() { const t = useTranslations('layout'); @@ -54,7 +53,7 @@ function Footer() { href='mailto:hackerspace-styret@idi.ntnu.no' aria-label={t('sendAnEmail')} > - + @@ -67,7 +66,7 @@ function Footer() { target='_blank' rel='noopener noreferrer' > - + @@ -80,7 +79,7 @@ function Footer() { target='_blank' rel='noopener noreferrer' > - + @@ -93,7 +92,7 @@ function Footer() { target='_blank' rel='noopener noreferrer' > - + @@ -106,7 +105,7 @@ function Footer() { target='_blank' rel='noopener noreferrer' > - + @@ -130,7 +129,7 @@ function Footer() { {t('signIn')}
- {t('haveYouFoundA')} ? + {t('haveYouFoundA')} ?
{t.rich('utilitiesDescription', { code: (children) => ( @@ -142,7 +141,7 @@ function Footer() { href='mailto:hackerspace-dev@idi.ntnu.no' aria-label={t('sendAnEmail')} > - + ), @@ -198,7 +197,7 @@ function Footer() { target='_blank' rel='noopener noreferrer' > - + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index adb8fec..c81eba8 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,11 +1,10 @@ -import { useTranslations } from 'next-intl'; - import { LogoLink } from '@/components/layout/LogoLink'; import { MobileSheet } from '@/components/layout/MobileSheet'; import { Nav } from '@/components/layout/Nav'; import { DarkModeMenu } from '@/components/settings/DarkModeMenu'; import { LocaleMenu } from '@/components/settings/LocaleMenu'; import { ProfileMenu } from '@/components/settings/ProfileMenu'; +import { useTranslations } from 'next-intl'; function Header() { const t = useTranslations('layout'); diff --git a/src/components/layout/LogoLink.tsx b/src/components/layout/LogoLink.tsx index 58c0c24..23018e8 100644 --- a/src/components/layout/LogoLink.tsx +++ b/src/components/layout/LogoLink.tsx @@ -1,10 +1,8 @@ -import { useTranslations } from 'next-intl'; - -import { Link } from '@/lib/navigation'; -import { cx } from '@/lib/utils'; - import { Logo } from '@/components/assets/Logo'; import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { cx } from '@/lib/utils'; +import { useTranslations } from 'next-intl'; function LogoLink({ className, diff --git a/src/components/layout/MobileSheet.tsx b/src/components/layout/MobileSheet.tsx index 2f0dc3c..12eddbd 100644 --- a/src/components/layout/MobileSheet.tsx +++ b/src/components/layout/MobileSheet.tsx @@ -1,8 +1,5 @@ 'use client'; -import { Menu } from 'lucide-react'; -import * as React from 'react'; - import { LogoLink } from '@/components/layout/LogoLink'; import { Nav } from '@/components/layout/Nav'; import { Button } from '@/components/ui/Button'; @@ -13,6 +10,8 @@ import { SheetTitle, SheetTrigger, } from '@/components/ui/Sheet'; +import { MenuIcon } from 'lucide-react'; +import * as React from 'react'; type MobileSheetProps = { className?: string; @@ -36,7 +35,7 @@ function MobileSheet({ className, t }: MobileSheetProps) { size='icon' aria-label={t.navigationMenu} > - + diff --git a/src/components/layout/Nav.tsx b/src/components/layout/Nav.tsx index 4654c96..3164282 100644 --- a/src/components/layout/Nav.tsx +++ b/src/components/layout/Nav.tsx @@ -1,6 +1,5 @@ -import { Link } from '@/lib/navigation'; - import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; type NavProps = { className?: string; diff --git a/src/components/layout/PaginationCarousel.tsx b/src/components/layout/PaginationCarousel.tsx index 7ff6326..bdc222c 100644 --- a/src/components/layout/PaginationCarousel.tsx +++ b/src/components/layout/PaginationCarousel.tsx @@ -1,9 +1,5 @@ 'use client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -import { cx } from '@/lib/utils'; - import { Pagination, PaginationContent, @@ -13,7 +9,8 @@ import { PaginationNext, PaginationPrevious, } from '@/components/ui/Pagination'; - +import { cx } from '@/lib/utils'; +import { parseAsInteger, useQueryState } from 'nuqs'; type PaginationCarouselProps = { className?: string; totalPages: number; diff --git a/src/components/layout/PaginationCarouselSkeleton.tsx b/src/components/layout/PaginationCarouselSkeleton.tsx index 3021e21..9e55181 100644 --- a/src/components/layout/PaginationCarouselSkeleton.tsx +++ b/src/components/layout/PaginationCarouselSkeleton.tsx @@ -1,5 +1,3 @@ -import { useTranslations } from 'next-intl'; - import { Pagination, PaginationContent, @@ -8,7 +6,7 @@ import { PaginationNext, PaginationPrevious, } from '@/components/ui/Pagination'; - +import { useTranslations } from 'next-intl'; type PaginationCarouselSkeletonProps = { className?: string; }; diff --git a/src/components/news/ArticleCard.tsx b/src/components/news/ArticleCard.tsx index f64ed5f..041f2ee 100644 --- a/src/components/news/ArticleCard.tsx +++ b/src/components/news/ArticleCard.tsx @@ -1,8 +1,3 @@ -import Image from 'next/image'; - -import { Link } from '@/lib/navigation'; -import { cx } from '@/lib/utils'; - import { InternalBadge } from '@/components/news/InternalBadge'; import { Button } from '@/components/ui/Button'; import { @@ -11,7 +6,9 @@ import { CardHeader, CardTitle, } from '@/components/ui/Card'; - +import { Link } from '@/lib/locale/navigation'; +import { cx } from '@/lib/utils'; +import Image from 'next/image'; type ArticleCardProps = { className?: string; id: number; diff --git a/src/components/news/ArticleItem.tsx b/src/components/news/ArticleItem.tsx index a18ea51..b1c774a 100644 --- a/src/components/news/ArticleItem.tsx +++ b/src/components/news/ArticleItem.tsx @@ -1,10 +1,8 @@ -import Image from 'next/image'; - -import { Link } from '@/lib/navigation'; -import { cx } from '@/lib/utils'; - import { InternalBadge } from '@/components/news/InternalBadge'; import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { cx } from '@/lib/utils'; +import Image from 'next/image'; type ArticleItemProps = { className?: string; diff --git a/src/components/news/CardGrid.tsx b/src/components/news/CardGrid.tsx index dc4b9db..46156cd 100644 --- a/src/components/news/CardGrid.tsx +++ b/src/components/news/CardGrid.tsx @@ -1,7 +1,5 @@ -import { cx } from '@/lib/utils'; - import { ArticleCard } from '@/components/news/ArticleCard'; - +import { cx } from '@/lib/utils'; type CardGridProps = { topArticles: { id: number; diff --git a/src/components/news/CardGridSkeleton.tsx b/src/components/news/CardGridSkeleton.tsx index 699fe5a..09a1392 100644 --- a/src/components/news/CardGridSkeleton.tsx +++ b/src/components/news/CardGridSkeleton.tsx @@ -1,8 +1,6 @@ -import * as React from 'react'; - -import { cx } from '@/lib/utils'; - import { Skeleton } from '@/components/ui/Skeleton'; +import { cx } from '@/lib/utils'; +import * as React from 'react'; function CardGridSkeleton() { return ( diff --git a/src/components/news/InternalBadge.tsx b/src/components/news/InternalBadge.tsx index 1eac30b..e2f0932 100644 --- a/src/components/news/InternalBadge.tsx +++ b/src/components/news/InternalBadge.tsx @@ -1,8 +1,3 @@ -import { ShieldAlert } from 'lucide-react'; -import { useTranslations } from 'next-intl'; - -import { cx } from '@/lib/utils'; - import { Button } from '@/components/ui/Button'; import { Tooltip, @@ -10,6 +5,9 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/Tooltip'; +import { cx } from '@/lib/utils'; +import { ShieldAlertIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; type InternalBadgeProps = { className?: string; @@ -33,7 +31,7 @@ function InternalBadge({ className, internal }: InternalBadgeProps) { size='xs-icon' > - + diff --git a/src/components/news/ItemGrid.tsx b/src/components/news/ItemGrid.tsx index 2a8a359..6a610f6 100644 --- a/src/components/news/ItemGrid.tsx +++ b/src/components/news/ItemGrid.tsx @@ -1,6 +1,5 @@ -import { articleMockData as articleData } from '@/mock-data/article'; - import { ArticleItem } from '@/components/news/ArticleItem'; +import { articleMockData as articleData } from '@/mock-data/article'; type ItemGridProps = { page: number; diff --git a/src/components/news/ItemGridSkeleton.tsx b/src/components/news/ItemGridSkeleton.tsx index 03567e1..51f4699 100644 --- a/src/components/news/ItemGridSkeleton.tsx +++ b/src/components/news/ItemGridSkeleton.tsx @@ -1,6 +1,5 @@ -import * as React from 'react'; - import { ArticleItemSkeleton } from '@/components/news/ArticleItemSkeleton'; +import * as React from 'react'; function ItemGridSkeleton() { return ( diff --git a/src/components/profile/AvatarIcon.tsx b/src/components/profile/AvatarIcon.tsx index 8925682..03a8e0c 100644 --- a/src/components/profile/AvatarIcon.tsx +++ b/src/components/profile/AvatarIcon.tsx @@ -1,6 +1,5 @@ -import Image from 'next/image'; - import { Avatar, AvatarFallback } from '@/components/ui/Avatar'; +import Image from 'next/image'; type AvatarIconProps = { className?: string; diff --git a/src/components/providers/RootProviders.tsx b/src/components/providers/RootProviders.tsx index 0f2502c..d4f5704 100644 --- a/src/components/providers/RootProviders.tsx +++ b/src/components/providers/RootProviders.tsx @@ -1,4 +1,5 @@ import { IntlErrorProvider } from '@/components/providers/IntlErrorProvider'; +import { TRPCProvider } from '@/components/providers/TRPCProvider'; import { ThemeProvider } from '@/components/providers/ThemeProvider'; type RootProvidersProps = { @@ -9,7 +10,9 @@ type RootProvidersProps = { function RootProviders({ children, locale }: RootProvidersProps) { return ( - {children} + + {children} + ); } diff --git a/src/components/providers/TRPCProvider.tsx b/src/components/providers/TRPCProvider.tsx new file mode 100644 index 0000000..f7761f1 --- /dev/null +++ b/src/components/providers/TRPCProvider.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { env } from '@/env'; +import { api } from '@/lib/api/client'; +import { createQueryClient } from '@/lib/api/queryClient'; +import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { loggerLink, unstable_httpBatchStreamLink } from '@trpc/client'; +import { useState } from 'react'; +import SuperJSON from 'superjson'; + +let clientQueryClientSingleton: QueryClient | undefined = undefined; +const getQueryClient = () => { + if (typeof window === 'undefined') { + // Server: always make a new query client + return createQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + if (!clientQueryClientSingleton) { + clientQueryClientSingleton = createQueryClient(); + } + return clientQueryClientSingleton; +}; + +function TRPCProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + api.createClient({ + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === 'development' || + (op.direction === 'down' && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + transformer: SuperJSON, + url: `${env.NEXT_PUBLIC_SITE_URL}/api/data`, + headers: () => { + const headers = new Headers(); + headers.set('x-trpc-source', 'nextjs-react'); + return headers; + }, + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} + +export { TRPCProvider }; diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index 6c06483..39919b9 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -1,18 +1,18 @@ 'use client'; -import { ThemeProvider } from 'next-themes'; +import { ThemeProvider as NextThemeProvider } from 'next-themes'; -function NextThemeProvider({ children }: { children: React.ReactNode }) { +function ThemeProvider({ children }: { children: React.ReactNode }) { return ( - {children} - + ); } -export { NextThemeProvider as ThemeProvider }; +export { ThemeProvider }; diff --git a/src/components/settings/DarkModeMenu.tsx b/src/components/settings/DarkModeMenu.tsx index ba1212b..09ea3ed 100644 --- a/src/components/settings/DarkModeMenu.tsx +++ b/src/components/settings/DarkModeMenu.tsx @@ -1,9 +1,5 @@ 'use client'; -import { Moon, Sun } from 'lucide-react'; -import { useTheme } from 'next-themes'; -import * as React from 'react'; - import { Button } from '@/components/ui/Button'; import { DropdownMenu, @@ -11,6 +7,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/DropdownMenu'; +import { MoonIcon, SunIcon } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import * as React from 'react'; type DarkModeMenuProps = { t: { @@ -28,8 +27,8 @@ function DarkModeMenu({ t }: DarkModeMenuProps) { diff --git a/src/components/settings/LocaleMenu.tsx b/src/components/settings/LocaleMenu.tsx index 5ff377e..19164e6 100644 --- a/src/components/settings/LocaleMenu.tsx +++ b/src/components/settings/LocaleMenu.tsx @@ -1,12 +1,5 @@ 'use client'; -import { Globe2 } from 'lucide-react'; -import { useParams } from 'next/navigation'; -import * as React from 'react'; - -import { flagIcons, locales } from '@/lib/config'; -import { usePathname, useRouter } from '@/lib/navigation'; - import { Button } from '@/components/ui/Button'; import { DropdownMenu, @@ -14,6 +7,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/DropdownMenu'; +import { localeIcons, routing } from '@/lib/locale'; +import { usePathname, useRouter } from '@/lib/locale/navigation'; +import { Globe2Icon } from 'lucide-react'; +import { useParams } from 'next/navigation'; +import * as React from 'react'; function LocaleMenu({ t }: { t: { changeLocale: string } }) { const router = useRouter(); @@ -23,13 +21,13 @@ function LocaleMenu({ t }: { t: { changeLocale: string } }) { - {locales.map((locale) => { - const FlagIcon = flagIcons[locale as keyof typeof flagIcons]; + {routing.locales.map((locale) => { + const FlagIcon = localeIcons[locale as keyof typeof localeIcons]; return ( diff --git a/src/components/ui/Avatar.tsx b/src/components/ui/Avatar.tsx index 6aa1374..a9166d5 100644 --- a/src/components/ui/Avatar.tsx +++ b/src/components/ui/Avatar.tsx @@ -1,10 +1,9 @@ 'use client'; +import { cx } from '@/lib/utils'; import * as AvatarPrimitive from '@radix-ui/react-avatar'; import * as React from 'react'; -import { cx } from '@/lib/utils'; - const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx index 1a4e2d4..c84e389 100644 --- a/src/components/ui/Badge.tsx +++ b/src/components/ui/Badge.tsx @@ -1,6 +1,5 @@ -import type * as React from 'react'; - import { type VariantProps, cva, cx } from '@/lib/utils'; +import type * as React from 'react'; const badgeVariants = cva({ base: 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 46e2c08..0a34580 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,8 +1,7 @@ +import { type VariantProps, cva } from '@/lib/utils'; import { Slot } from '@radix-ui/react-slot'; import * as React from 'react'; -import { type VariantProps, cva } from '@/lib/utils'; - const buttonVariants = cva({ base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', variants: { diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 0682009..0479465 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,6 +1,5 @@ -import * as React from 'react'; - import { cx } from '@/lib/utils'; +import * as React from 'react'; const Card = React.forwardRef< HTMLDivElement, diff --git a/src/components/ui/Combobox.tsx b/src/components/ui/Combobox.tsx index 9fe99fe..57a4b93 100644 --- a/src/components/ui/Combobox.tsx +++ b/src/components/ui/Combobox.tsx @@ -1,10 +1,5 @@ 'use client'; -import { Check, ChevronsUpDown } from 'lucide-react'; -import * as React from 'react'; - -import { cx } from '@/lib/utils'; - import { Button } from '@/components/ui/Button'; import { Command, @@ -19,6 +14,9 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/Popover'; +import { cx } from '@/lib/utils'; +import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; +import * as React from 'react'; type ComboboxProps = { choices: { @@ -53,7 +51,7 @@ function Combobox({ {value ? choices.find((choice) => choice.value === value)?.label : defaultDescription} - + @@ -71,7 +69,7 @@ function Combobox({ setOpen(false); }} > - , React.ComponentPropsWithoutRef @@ -43,7 +41,7 @@ const CommandInput = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => (
- + {children} - + Close diff --git a/src/components/ui/DropdownMenu.tsx b/src/components/ui/DropdownMenu.tsx index de391cc..c7daf18 100644 --- a/src/components/ui/DropdownMenu.tsx +++ b/src/components/ui/DropdownMenu.tsx @@ -1,7 +1,7 @@ 'use client'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; -import { Check, ChevronRight, Circle } from 'lucide-react'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; import * as React from 'react'; import { cx } from '@/lib/utils'; @@ -34,7 +34,7 @@ const DropdownMenuSubTrigger = React.forwardRef< {...props} > {children} - + )); DropdownMenuSubTrigger.displayName = @@ -107,7 +107,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< > - + {children} @@ -130,7 +130,7 @@ const DropdownMenuRadioItem = React.forwardRef< > - + {children} diff --git a/src/components/ui/Pagination.tsx b/src/components/ui/Pagination.tsx index ffbcc6f..64cd132 100644 --- a/src/components/ui/Pagination.tsx +++ b/src/components/ui/Pagination.tsx @@ -1,9 +1,11 @@ -import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; -import * as React from 'react'; - -import { cx } from '@/lib/utils'; - import { type ButtonProps, buttonVariants } from '@/components/ui/Button'; +import { cx } from '@/lib/utils'; +import { + ChevronLeftIcon, + ChevronRightIcon, + MoreHorizontalIcon, +} from 'lucide-react'; +import * as React from 'react'; const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (