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}
+
+
+
+
+
+
{author.name}
+
+ {t('readTime', { count: Math.ceil(minutes) })}
+ •
+ {article.date}
+
+
+
+ {article.views + ' ' + t('views')}
+
+
+
+ );
+}
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 (
-