diff --git a/.dockerignore b/.dockerignore index 4289648..b909791 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,9 +23,6 @@ next-env.d.ts public/sitemap.xml public/robots.txt -# prettier -prettier.config.js - # docker Dockerfile* .dockerignore diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7ec4949..c9493e4 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,10 +9,9 @@ const config = { 'next/core-web-vitals', 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:prettier/recommended', ], rules: { - // These opinionated rules are enabled in stylistic-type-checked above. - // Feel free to reconfigure them to your own preference. '@typescript-eslint/array-type': 'off', '@typescript-eslint/consistent-type-definitions': 'off', diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint.yml similarity index 76% rename from .github/workflows/lint-and-format.yml rename to .github/workflows/lint.yml index dd3f2b1..79344d8 100644 --- a/.github/workflows/lint-and-format.yml +++ b/.github/workflows/lint.yml @@ -1,8 +1,8 @@ -name: Lint and Format +name: Lint on: push jobs: - lint-and-format: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -11,4 +11,3 @@ jobs: bun-version: 1.0.24 - run: bun install - run: bun lint - - run: bun format diff --git a/.gitignore b/.gitignore index 7d40eb0..4752b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a612eaa..a294968 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,6 @@ { "recommendations": [ - "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", "bradlc.vscode-tailwindcss", "lokalise.i18n-ally", "vivaxy.vscode-conventional-commits", diff --git a/.vscode/settings.json b/.vscode/settings.json index ee4bfab..e8f66e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,5 @@ ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ], "i18n-ally.localesPaths": ["messages"], - "i18n-ally.keystyle": "nested", - "codeQL.githubDatabase.download": "never" + "i18n-ally.keystyle": "nested" } diff --git a/Dockerfile b/Dockerfile index 0798ba3..deda379 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM imbios/bun-node:20-alpine +FROM imbios/bun-node:20-slim WORKDIR /app COPY package.json bun.lockb ./ -RUN bun install --frozen-lockfile --production +RUN bun install --production COPY . . diff --git a/README.md b/README.md index bab85eb..6ffe7d1 100644 --- a/README.md +++ b/README.md @@ -63,26 +63,12 @@ Then to serve the build locally, run: bun run start ``` -## Check linting, formatting and types +## Check linting and formatting -To check linting, formatting or types you run the respective command: - -Linting: - -```bash -bun run lint -``` - -Formatting: - -```bash -bun run format -``` - -Types: +To check linting and formatting you run the respective command: ```bash -bun run type +bun lint ``` If you are using vscode and are experiencing issues with types, you can restart the typescript server by pressing `cmd + shift + p` and then type `TypeScript: Restart TS Server` (You need to have a typescript file open for this to work). diff --git a/bun.lockb b/bun.lockb index 1d36ca4..78474c9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/messages/en.json b/messages/en.json index 44f1e18..e115929 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8,7 +8,8 @@ "goToPreviousPage": "Go to previous page", "next": "Next", "goToNextPage": "Go to next page", - "morePages": "More pages" + "morePages": "More pages", + "page": "page" }, "layout": { "hackerspaceHome": "Hackerspace homepage", @@ -42,8 +43,9 @@ }, "news": { "title": "News", - "page": "Page", "internalArticle": "This is an internal article", - "newArticle": "New article" + "newArticle": "New article", + "readTime": "{count, plural, =0 {less than a minute} one {# minute} other {# minutes}} read", + "views": "Views" } } diff --git a/messages/no.json b/messages/no.json index ba6a4bd..b8e079c 100644 --- a/messages/no.json +++ b/messages/no.json @@ -8,7 +8,8 @@ "goToPreviousPage": "Gå til forrige side", "next": "Neste", "goToNextPage": "Gå til neste side", - "morePages": "Flere sider" + "morePages": "Flere sider", + "page": "side" }, "layout": { "hackerspaceHome": "Hackerspace hjemmeside", @@ -42,8 +43,9 @@ }, "news": { "title": "Nyheter", - "page": "Side", "internalArticle": "Dette er en intern artikkel", - "newArticle": "Ny artikkel" + "newArticle": "Ny artikkel", + "readTime": "{count, plural, =0 {mindre enn ett minutt} one {# minutt} other {# minutter}} read", + "views": "Visninger" } } diff --git a/package.json b/package.json index 71b8fb8..e81423e 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,15 @@ "build": "next build", "dev": "next dev", "lint": "next lint", - "format": "prettier --check '**/*.{js,cjs,ts,tsx}'", - "type": "tsc --noEmit", "start": "next start" }, "lint-staged": { "*.{js,cjs,ts,tsx}": [ - "eslint --fix", - "prettier --write" + "eslint --fix" ] }, "dependencies": { + "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-separator": "^1.0.3", @@ -30,6 +28,7 @@ "clsx": "^2.1.0", "country-flag-icons": "^1.5.9", "cva": "^1.0.0-beta.1", + "husky": "^9.0.10", "lucide-react": "^0.312.0", "next": "^14.0.4", "next-intl": "^3.4.4", @@ -38,9 +37,9 @@ "nuqs": "^1.15.4", "react": "18.2.0", "react-dom": "18.2.0", + "reading-time": "^1.5.0", + "sharp": "^0.33.2", "tailwind-merge": "^2.2.0", - "tailwind-scrollbar": "^3.0.5", - "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" }, "devDependencies": { @@ -53,12 +52,15 @@ "@typescript-eslint/parser": "^6.11.0", "eslint": "^8.54.0", "eslint-config-next": "^14.0.4", - "husky": "^8.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "lint-staged": "^15.2.0", "postcss": "^8.4.31", "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.7", + "tailwind-scrollbar": "^3.0.5", "tailwindcss": "^3.3.5", + "tailwindcss-animate": "^1.0.7", "typescript": "^5.1.6" }, "packageManager": "bun@1.0.24" diff --git a/public/authorMock.jpg b/public/authorMock.jpg new file mode 100644 index 0000000..c3d5590 Binary files /dev/null and b/public/authorMock.jpg differ diff --git a/public/favicon/site.webmanifest b/public/favicon/site.webmanifest index 93f9971..4962fdc 100644 --- a/public/favicon/site.webmanifest +++ b/public/favicon/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#0c0a09", - "background_color": "#0c0a09", - "display": "standalone" + "name": "Hackerspace NTNU", + "short_name": "Hackerspace", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#0c0a09", + "background_color": "#0c0a09", + "display": "standalone" } diff --git a/src/app/[locale]/(dashboard)/layout.tsx b/src/app/[locale]/(dashboard)/layout.tsx index ff5917e..ab8e36b 100644 --- a/src/app/[locale]/(dashboard)/layout.tsx +++ b/src/app/[locale]/(dashboard)/layout.tsx @@ -1,17 +1,18 @@ import { unstable_setRequestLocale } from 'next-intl/server'; -import { type ReactNode } from 'react'; import { Footer } from '@/components/layout/Footer'; import { Header } from '@/components/layout/Header'; import { Main } from '@/components/layout/Main'; -export default function Dashboardlayout({ +type DashboardProps = { + children: React.ReactNode; + params: { locale: string }; +}; + +export default function Dashboard({ children, params: { locale }, -}: { - children: ReactNode; - params: { locale: string }; -}) { +}: DashboardProps) { unstable_setRequestLocale(locale); return ( <> diff --git a/src/app/[locale]/(dashboard)/news/(header)/layout.tsx b/src/app/[locale]/(dashboard)/news/(header)/layout.tsx new file mode 100644 index 0000000..61e0a23 --- /dev/null +++ b/src/app/[locale]/(dashboard)/news/(header)/layout.tsx @@ -0,0 +1,34 @@ +import { SquarePen } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; + +import { Link } from '@/lib/navigation'; + +import { Button } from '@/components/ui/Button'; + +type NewsHeaderProps = { + children: React.ReactNode; + params: { locale: string }; +}; + +export default function NewsHeader({ + children, + params: { locale }, +}: NewsHeaderProps) { + unstable_setRequestLocale(locale); + const t = useTranslations('news'); + return ( + <> +
+

{t('title')}

+ +
+ {children} + + ); +} diff --git a/src/app/[locale]/(dashboard)/news/(header)/loading.tsx b/src/app/[locale]/(dashboard)/news/(header)/loading.tsx new file mode 100644 index 0000000..0883f7d --- /dev/null +++ b/src/app/[locale]/(dashboard)/news/(header)/loading.tsx @@ -0,0 +1,15 @@ +import { PaginationCarouselSkeleton } from '@/components/layout/PaginationCarouselSkeleton'; +import { CardGridSkeleton } from '@/components/news/CardGridSkeleton'; +import { ItemGridSkeleton } from '@/components/news/ItemGridSkeleton'; +import { Separator } from '@/components/ui/Separator'; + +export default function NewsSkeleton() { + return ( + <> + + + + + + ); +} diff --git a/src/app/[locale]/(dashboard)/news/(header)/page.tsx b/src/app/[locale]/(dashboard)/news/(header)/page.tsx new file mode 100644 index 0000000..7d30c05 --- /dev/null +++ b/src/app/[locale]/(dashboard)/news/(header)/page.tsx @@ -0,0 +1,61 @@ +import { articleMockData as articleData } from '@/mock-data/article'; +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { createSearchParamsCache, parseAsInteger } from 'nuqs/parsers'; +import { Suspense } from 'react'; + +import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; +import { CardGrid } from '@/components/news/CardGrid'; +import { ItemGrid } from '@/components/news/ItemGrid'; +import { ItemGridSkeleton } from '@/components/news/ItemGridSkeleton'; +import { Separator } from '@/components/ui/Separator'; + +export async function generateMetadata({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const t = await getTranslations({ locale, namespace: 'layout' }); + + return { + title: t('news'), + }; +} + +export default function News({ + params: { locale }, + searchParams, +}: { + params: { locale: string }; + searchParams: Record; +}) { + unstable_setRequestLocale(locale); + const t = useTranslations('ui'); + const searchParamsCache = createSearchParamsCache({ + [t('page')]: parseAsInteger.withDefault(1), + }); + + const { [t('page')]: page = 1 } = searchParamsCache.parse(searchParams); + // TODO: Button to create new article should only be visible when logged in + return ( + <> + + + }> + + + + + ); +} diff --git a/src/app/[locale]/(dashboard)/news/[article]/page.tsx b/src/app/[locale]/(dashboard)/news/[article]/page.tsx new file mode 100644 index 0000000..c5f7558 --- /dev/null +++ b/src/app/[locale]/(dashboard)/news/[article]/page.tsx @@ -0,0 +1,87 @@ +import { + articleMockData as articleData, + authorMockData as authorData, +} from '@/mock-data/article'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import Image from 'next/image'; +import { notFound } from 'next/navigation'; +import readingTime from 'reading-time'; + +import { AvatarIcon } from '@/components/profile/AvatarIcon'; +import { Badge } from '@/components/ui/Badge'; + +// export async function generateStaticParams() { +// return articleData.map((article) => ({ +// article: String(article.id), +// })); +// } + +export async function generateMetadata({ + params, +}: { + params: { article: string }; +}) { + const article = articleData.find( + (article) => article.id === Number(params.article), + ); + + return { + title: article?.title, + }; +} + +export default function Article({ + params, +}: { + params: { locale: string; article: string }; +}) { + unstable_setRequestLocale(params.locale); + const t = useTranslations('news'); + + const article = articleData.find( + (article) => article.id === Number(params.article), + ); + if (!article) { + return notFound(); + } + + const { minutes } = readingTime(article.content!); + const author = authorData[0]!; + return ( +
+
+
+ {article.title} +
+

{article.title}

+
+
+
+ +
+

{author.name}

+ + {t('readTime', { count: Math.ceil(minutes) })} +   •   + {article.date} + +
+
+ {article.views + ' ' + t('views')} +
+
{article.content}
+
+ ); +} diff --git a/src/app/[locale]/(dashboard)/news/page.tsx b/src/app/[locale]/(dashboard)/news/page.tsx deleted file mode 100644 index 3564e60..0000000 --- a/src/app/[locale]/(dashboard)/news/page.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { SquarePen } from 'lucide-react'; -import { useTranslations } from 'next-intl'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; - -import { Link } from '@/lib/navigation'; -import { cx } from '@/lib/utils'; - -import { NewsCard } from '@/components/news/NewsCard'; -import { NewsItemGrid } from '@/components/news/NewsItemGrid'; -import { Button } from '@/components/ui/Button'; -import { Separator } from '@/components/ui/Separator'; - -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { - const t = await getTranslations({ locale, namespace: 'layout' }); - - return { - title: t('news'), - }; -} - -export default function News({ - params: { locale }, -}: { - params: { locale: string }; -}) { - const mockData = [ - { - id: 1, - internal: true, - title: 'Gruppe status: prosjekt spill', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 2, - internal: false, - title: 'DevOps Møtet', - date: '69. oktober 6969', - photoUrl: 'mock.jpg', - }, - { - id: 3, - internal: false, - title: 'Jonas er kul', - date: '42. november 2023', - photoUrl: 'mock.jpg', - }, - { - id: 4, - internal: true, - title: 'Iskrem er godt', - date: '18. februar 1942', - photoUrl: 'mock.jpg', - }, - { - id: 5, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 6, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 7, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 8, - internal: false, - title: 'Dette er en veeeeldig lang overskrift som skal testes', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 9, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 10, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 11, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 12, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 13, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 14, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 15, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 16, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 17, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 18, - internal: false, - title: '18', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - - { - id: 19, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 20, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 21, - internal: false, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 22, - internal: true, - title: 'Hvorfor er jeg her?', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - { - id: 23, - internal: false, - title: '23', - date: '22. oktober 2023', - photoUrl: 'mock.jpg', - }, - ]; - unstable_setRequestLocale(locale); - const t = useTranslations('news'); - // TODO: Button to create new article should only be visible when logged in - return ( - <> -
-

{t('title')}

- -
-
- {mockData.slice(0, 4).map((data, index) => ( - - ))} -
- - - - ); -} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 7cc20b1..26d239d 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,15 +1,14 @@ import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; import { Inter, Montserrat } from 'next/font/google'; import { notFound } from 'next/navigation'; -import { type ReactNode } from 'react'; import { locales } from '@/lib/config'; import { cx } from '@/lib/utils'; import { RootProviders } from '@/components/providers/RootProviders'; -type Props = { - children: ReactNode; +type LocalelayoutProps = { + children: React.ReactNode; params: { locale: string }; }; @@ -31,12 +30,12 @@ export function generateStaticParams() { export async function generateMetadata({ params: { locale }, -}: Omit) { +}: Omit) { const t = await getTranslations({ locale, namespace: 'meta' }); return { title: { - template: 'Hackerspace NTNU | %s', + template: '%s | Hackerspace NTNU', default: 'Hackerspace NTNU', }, description: t('description'), @@ -72,7 +71,10 @@ export async function generateMetadata({ }; } -export default function Localelayout({ children, params: { locale } }: Props) { +export default function Localelayout({ + children, + params: { locale }, +}: LocalelayoutProps) { if (!locales.includes(locale)) notFound(); unstable_setRequestLocale(locale); return ( @@ -83,7 +85,7 @@ export default function Localelayout({ children, params: { locale } }: Props) { suppressHydrationWarning > - +
{children}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b331630..b2a2525 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,9 @@ -import { type ReactNode } from 'react'; - import '@/styles/globals.css'; -type Props = { - children: ReactNode; +type RootlayoutProps = { + children: React.ReactNode; }; -export default function Rootlayout({ children }: Props) { +export default function Rootlayout({ children }: RootlayoutProps) { return children; } diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 4d4c7c2..815a0b6 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -18,196 +18,196 @@ function Footer() { const t = useTranslations('layout'); const year = new Date().getFullYear(); return ( -
-
-
-
+
+
+
+
-

- {t('openingHours')}: -
- {t('allWeekdays')}, 10:15-18:00 -
-
-

+

+ {t('openingHours')}: +
+ {t('allWeekdays')}, 10:15-18:00 +
+
+ +
+ Høgskoleringen 5
+ 7034 Trondheim +

+
+
+

{t('socialMedia')}

+
    +
  • + +
  • +
  • + -
    - Høgskoleringen 5
    - 7034 Trondheim -

    -
-
-

{t('socialMedia')}

-
    -
  • -
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+

{t('links')}

+
+
+

{t('utilities')}

+

+ +
+ {t('haveYouFoundA')} ? +
+ {t.rich('utilitiesDescription', { + code: (children) => ( + {children} + ), + MailLink: () => ( + - -

  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • - -
    -
    -

    {t('links')}

    -
    -
    -

    {t('utilities')}

    -

    - -
    - {t('haveYouFoundA')} ? -
    - {t.rich('utilitiesDescription', { - code: (children) => ( - {children} - ), - MailLink: () => ( - - ), - SlackLink: (children) => ( - - ), - GithubLink: (children) => ( - - ), - })} -

    -
    + ), + })} +

    - -

    - {t('copyright')} © {year}, Hackerspace NTNU.{' '} - {t('allRightsReserved')}. -

    + +

    + {t('copyright')} © {year}, Hackerspace NTNU.{' '} + {t('allRightsReserved')}. +

    ); } diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 773b957..6812b8d 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -10,48 +10,46 @@ import { ProfileMenu } from '@/components/settings/ProfileMenu'; function Header() { const t = useTranslations('layout'); return ( -
    -
    - + + +
    +
    diff --git a/src/components/layout/Main.tsx b/src/components/layout/Main.tsx index d27ef7b..5b9de33 100644 --- a/src/components/layout/Main.tsx +++ b/src/components/layout/Main.tsx @@ -1,27 +1,19 @@ -'use client'; - -import { type ReactNode } from 'react'; - import { cx } from '@/lib/utils'; type MainProps = { - children?: ReactNode; - mainClassName?: string; + children?: React.ReactNode; className?: string; }; -function Main({ children, mainClassName, className }: MainProps) { +function Main({ children, className }: MainProps) { return ( -
    -
    - {children} -
    +
    + {children}
    ); } diff --git a/src/components/layout/PaginationCarousel.tsx b/src/components/layout/PaginationCarousel.tsx index ebf99b7..8df4bd1 100644 --- a/src/components/layout/PaginationCarousel.tsx +++ b/src/components/layout/PaginationCarousel.tsx @@ -1,3 +1,7 @@ +'use client'; + +import { parseAsInteger, useQueryState } from 'nuqs'; + import { cx } from '@/lib/utils'; import { @@ -12,8 +16,6 @@ import { type PaginationCarouselProps = { className?: string; - page: number; - setPage: (page: number) => void; totalPages: number; t: { goToPreviousPage: string; @@ -21,36 +23,40 @@ type PaginationCarouselProps = { morePages: string; goToNextPage: string; next: string; + page: string; }; }; function PaginationCarousel({ className, - page, - setPage, totalPages, t, }: PaginationCarouselProps) { - async function handlePrevious(e: React.MouseEvent) { + const [page, setPage] = useQueryState( + t.page, + parseAsInteger.withDefault(1).withOptions({ shallow: false }), + ); + + function handlePrevious(e: React.MouseEvent) { e.preventDefault(); if (page > 1) { - setPage(page - 1); + void setPage(page - 1); } } - async function handleNext(e: React.MouseEvent) { + function handleNext(e: React.MouseEvent) { e.preventDefault(); if (page < totalPages) { - setPage(page + 1); + void setPage(page + 1); } } - async function handlePageClick( + function handlePageClick( e: React.MouseEvent, pageNum: number, ) { e.preventDefault(); - setPage(pageNum); + void setPage(pageNum); } let pagesToDisplay = []; diff --git a/src/components/layout/PaginationCarouselSkeleton.tsx b/src/components/layout/PaginationCarouselSkeleton.tsx new file mode 100644 index 0000000..60c6963 --- /dev/null +++ b/src/components/layout/PaginationCarouselSkeleton.tsx @@ -0,0 +1,53 @@ +import { useTranslations } from 'next-intl'; + +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/Pagination'; + +type PaginationCarouselSkeletonProps = { + className?: string; +}; + +function PaginationCarouselSkeleton({ + className, +}: PaginationCarouselSkeletonProps) { + const t = useTranslations('ui'); + return ( + + + + + + {Array.from({ length: 4 }).map((_, index) => ( + + + + ))} + + + + + + ); +} + +export { PaginationCarouselSkeleton }; diff --git a/src/components/news/NewsCard.tsx b/src/components/news/ArticleCard.tsx similarity index 85% rename from src/components/news/NewsCard.tsx rename to src/components/news/ArticleCard.tsx index e16cb23..f64ed5f 100644 --- a/src/components/news/NewsCard.tsx +++ b/src/components/news/ArticleCard.tsx @@ -12,27 +12,23 @@ import { CardTitle, } from '@/components/ui/Card'; -type NewsCardProps = { +type ArticleCardProps = { className?: string; id: number; internal: boolean; title: string; date: string; photoUrl: string; - t: { - internalArticle: string; - }; }; -function NewsCard({ +function ArticleCard({ className, id, internal, title, date, photoUrl, - t, -}: NewsCardProps) { +}: ArticleCardProps) { return ( -

    {t.internalArticle}

    +

    {t('internalArticle')}

    diff --git a/src/components/news/ItemGrid.tsx b/src/components/news/ItemGrid.tsx new file mode 100644 index 0000000..2a8a359 --- /dev/null +++ b/src/components/news/ItemGrid.tsx @@ -0,0 +1,32 @@ +import { articleMockData as articleData } from '@/mock-data/article'; + +import { ArticleItem } from '@/components/news/ArticleItem'; + +type ItemGridProps = { + page: number; +}; + +function ItemGrid({ page }: ItemGridProps) { + const itemsDisplayedAsCards = 4; + const itemsPerPage = 6; + + const start = (page - 1) * itemsPerPage + itemsDisplayedAsCards; + const end = start + itemsPerPage; + const currentData = articleData.slice(start, end); + return ( +
    + {currentData.map((data) => ( + + ))} +
    + ); +} + +export { ItemGrid, type ItemGridProps }; diff --git a/src/components/news/ItemGridSkeleton.tsx b/src/components/news/ItemGridSkeleton.tsx new file mode 100644 index 0000000..3f0b73e --- /dev/null +++ b/src/components/news/ItemGridSkeleton.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +import { ArticleItemSkeleton } from '@/components/news/ArticleItemSkeleton'; + +function ItemGridSkeleton() { + return ( +
    + {Array.from({ length: 6 }).map((_, index) => ( + + ))} +
    + ); +} + +export { ItemGridSkeleton }; diff --git a/src/components/news/NewsItemGrid.tsx b/src/components/news/NewsItemGrid.tsx deleted file mode 100644 index 23e4616..0000000 --- a/src/components/news/NewsItemGrid.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client'; - -import { parseAsInteger, useQueryState } from 'nuqs'; - -import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; -import { NewsItem } from '@/components/news/NewsItem'; - -type NewsItemGridProps = { - newsData: { - id: number; - internal: boolean; - title: string; - date: string; - photoUrl: string; - }[]; - t: { - morePages: string; - goToPreviousPage: string; - goToNextPage: string; - previous: string; - next: string; - page: string; - internalArticle: string; - }; -}; - -function NewsItemGrid({ newsData, t }: NewsItemGridProps) { - const itemsDisplayedAsCards = 4; - const itemsPerPage = 6; - const [page, setPage] = useQueryState(t.page, parseAsInteger.withDefault(1)); - - const start = (page - 1) * itemsPerPage + itemsDisplayedAsCards; - const end = start + itemsPerPage; - const currentData = newsData.slice(start, end); - - const totalPages = Math.ceil( - (newsData.length - itemsDisplayedAsCards) / itemsPerPage, - ); - - return ( - <> -
    - {currentData.map((data) => ( - - ))} -
    - - - ); -} - -export { NewsItemGrid }; diff --git a/src/components/profile/AvatarIcon.tsx b/src/components/profile/AvatarIcon.tsx new file mode 100644 index 0000000..8925682 --- /dev/null +++ b/src/components/profile/AvatarIcon.tsx @@ -0,0 +1,29 @@ +import Image from 'next/image'; + +import { Avatar, AvatarFallback } from '@/components/ui/Avatar'; + +type AvatarIconProps = { + className?: string; + photoUrl: string; + name: string; + initials: string; +}; + +function AvatarIcon({ className, photoUrl, name, initials }: AvatarIconProps) { + return ( + + {photoUrl && ( + {name} + )} + {!photoUrl && {initials}} + + ); +} + +export { AvatarIcon }; diff --git a/src/components/providers/IntlErrorProvider.tsx b/src/components/providers/IntlErrorProvider.tsx index e554675..afead1f 100644 --- a/src/components/providers/IntlErrorProvider.tsx +++ b/src/components/providers/IntlErrorProvider.tsx @@ -1,12 +1,11 @@ import { NextIntlClientProvider, useMessages } from 'next-intl'; -import { type ReactNode } from 'react'; type Props = { - children: ReactNode; - params: { locale: string }; + children: React.ReactNode; + locale: string; }; -function IntlErrorProvider({ children, params: { locale } }: Props) { +function IntlErrorProvider({ children, locale }: Props) { const messages = useMessages(); return ( - - {children} - + {children} ); } diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index 0ee14af..6c06483 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -1,9 +1,8 @@ 'use client'; import { ThemeProvider } from 'next-themes'; -import { type ReactNode } from 'react'; -function NextThemeProvider({ children }: { children: ReactNode }) { +function NextThemeProvider({ children }: { children: React.ReactNode }) { return ( , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarFallback }; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..1a7582a --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import { type VariantProps, cva, cx } from '@/lib/utils'; + +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', + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', + secondary: + 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', + destructive: + 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', + outline: 'text-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, +}); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ); +} + +export { Badge, badgeVariants }; diff --git a/src/design/Field.tsx b/src/design/Field.tsx index 698172b..dc152d2 100644 --- a/src/design/Field.tsx +++ b/src/design/Field.tsx @@ -1,5 +1,5 @@ import { Eye, EyeOff } from 'lucide-react'; -import React, { useState } from 'react'; +import * as React from 'react'; import { cx } from '@/lib/utils'; @@ -57,7 +57,7 @@ const Field: React.FC = ({ id, onClick, }) => { - const [showPassword, setShowPassword] = useState(false); + const [showPassword, setShowPassword] = React.useState(false); const _onChange = (event: React.ChangeEvent) => { if (onChange) { diff --git a/src/design/TypeWriter.tsx b/src/design/TypeWriter.tsx index d561d08..1a634e9 100644 --- a/src/design/TypeWriter.tsx +++ b/src/design/TypeWriter.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import * as React from 'react'; // TypeWriter Interface interface TypeWriterProps { @@ -9,10 +9,10 @@ interface TypeWriterProps { // TypeWriter Function: Renders text as if it is being written out. const TypeWriter: React.FC = ({ className, text, delay }) => { - const [currentText, setCurrentText] = useState(''); - const [currentIndex, setCurrentIndex] = useState(0); + const [currentText, setCurrentText] = React.useState(''); + const [currentIndex, setCurrentIndex] = React.useState(0); - useEffect(() => { + React.useEffect(() => { if (currentIndex < text.length) { const timeout = setTimeout(() => { setCurrentText((prevText) => prevText + text[currentIndex]); diff --git a/src/lib/config.ts b/src/lib/config.ts index e6aca37..fe6993f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -22,9 +22,9 @@ const pathnames = { en: '/news/new', no: '/nyheter/ny', }, - '/news/[articleId]': { - en: '/news/[articleId]', - no: '/nyheter/[articleId]', + '/news/[article]': { + en: '/news/[article]', + no: '/nyheter/[article]', }, '/about': { en: '/about', diff --git a/src/mock-data/article.ts b/src/mock-data/article.ts new file mode 100644 index 0000000..74f69c9 --- /dev/null +++ b/src/mock-data/article.ts @@ -0,0 +1,184 @@ +const articleMockData = [ + { + id: 1, + internal: true, + title: 'Gruppe status: prosjekt spill', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + views: 420, + content: + 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Officiis aut perferendis a, deleniti accusantium amet sunt autem eligendi repellat soluta omnis, nisi quam at vero perspiciatis. Ex repellendus saepe excepturi. Quam repellendus culpa quia facilis, exercitationem ipsa voluptatem nostrum aut libero labore quisquam est sed odio modi, eius quaerat tenetur deserunt facere officiis odit quibusdam consequuntur, rem vel similique? Nesciunt. Possimus libero ab suscipit enim quia. Error rerum architecto quidem ad voluptates distinctio minima tempore vel veniam esse ipsum officia atque, voluptatem molestias magni corrupti ducimus, placeat sint blanditiis praesentium? Cumque dignissimos totam pariatur repellat quod, vitae alias nostrum! Porro sequi mollitia blanditiis nulla accusantium fugiat explicabo! Soluta rerum debitis voluptates. Esse asperiores soluta facere fuga? Quas facilis nam inventore! Reiciendis tempora autem commodi dolor in doloremque eius a veritatis doloribus aliquid. Animi ipsam voluptatum sequi, eveniet placeat laboriosam iure, ullam ex odio reiciendis dicta enim libero, cupiditate et? Non. Quis ut eos, quod laboriosam suscipit exercitationem ratione incidunt blanditiis animi veritatis. Quos possimus exercitationem dolor inventore, esse ipsum, quod placeat provident officia et ab nihil? Modi impedit soluta eveniet. Sit qui cupiditate mollitia corrupti, sapiente neque enim vel praesentium veritatis voluptatibus? Laudantium sit nulla assumenda! Esse obcaecati sint dolores quos dolorum aliquam cum excepturi autem ad, fuga culpa veritatis! Quo nisi accusantium voluptatibus ipsam quia, ratione consectetur cupiditate adipisci sequi, nobis ab animi dolorem hic. Voluptates repellat ut molestias harum eos illo, odio sapiente doloribus, minima quidem, reprehenderit eum. Optio ut repellendus repudiandae at odit! Voluptates quidem eos perferendis amet veritatis quo excepturi fuga ipsa sunt quod facilis saepe, libero ea, neque cupiditate. Sint inventore laudantium error? Consectetur, porro. Laborum id assumenda, repellat ipsam cupiditate dolorum provident quod nostrum beatae a praesentium sequi animi corporis consequuntur. Atque inventore porro eum vitae? Architecto, officiis fugit tempora deserunt temporibus totam tenetur!', + }, + { + id: 2, + internal: false, + title: 'DevOps Møtet', + date: '69. oktober 6969', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 3, + internal: false, + title: 'Jonas er kul', + date: '42. november 2023', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 4, + internal: true, + title: 'Iskrem er godt', + date: '18. februar 1942', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 5, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 6, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 7, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 8, + internal: false, + title: 'Dette er en veeeeldig lang overskrift som skal testes', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + views: 420, + }, + { + id: 9, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 10, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 11, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 12, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 13, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 14, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 15, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 16, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 17, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 18, + internal: false, + title: '18', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + + { + id: 19, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 20, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 21, + internal: false, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 22, + internal: true, + title: 'Hvorfor er jeg her?', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, + { + id: 23, + internal: false, + title: '23', + date: '22. oktober 2023', + photoUrl: 'mock.jpg', + }, +]; + +const authorMockData = [ + { + name: 'Michael Jackson', + initials: 'MJ', + photoUrl: 'authorMock.jpg', + }, +]; + +export { articleMockData, authorMockData }; diff --git a/tailwind.config.ts b/tailwind.config.ts index 44fc8ce..7e5b270 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,7 +1,9 @@ +import tailwindScrollbar from 'tailwind-scrollbar'; import { type Config } from 'tailwindcss'; +import tailwindAnimate from 'tailwindcss-animate'; import { fontFamily } from 'tailwindcss/defaultTheme'; -export default { +const config = { content: ['./src/**/*.tsx'], darkMode: 'class', theme: { @@ -69,9 +71,7 @@ export default { }, }, }, - plugins: [ - require('tailwindcss-animate'), - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires - require('tailwind-scrollbar')({ nocompatible: true }), - ], + plugins: [tailwindAnimate, tailwindScrollbar({ nocompatible: true })], } satisfies Config; + +export default config; diff --git a/tsconfig.json b/tsconfig.json index c5eef6e..26da54a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,8 +26,8 @@ /* Path Aliases */ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] - } + "@/*": ["./src/*"], + }, }, "include": [ ".eslintrc.cjs", @@ -36,7 +36,7 @@ "**/*.tsx", "**/*.cjs", "**/*.js", - ".next/types/**/*.ts" + ".next/types/**/*.ts", ], - "exclude": ["node_modules"] + "exclude": ["node_modules"], }