Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Store nav items in database #121

Merged
merged 18 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ node_modules
npm-debug.log
README.md
.next
.git
.git
media
.env*
5 changes: 2 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ NEXTAUTH_URL=http://localhost:3000/api/auth
MEDIA_PATH="./media"
ADMIN_GROUPS="styrit"
CORPORATE_RELATIONS_GROUP="armit"
OLD_DATABASE_URL="mysql://user:password@localhost:3306/test"
OLD_GAMMA_API_KEY=""
#This is to ensure that the dev enviroment is the same as the production enviroment. Hydration issues can be missed i dev withouth this.
BASE_URL="http://localhost:3000"
# Ensures that the dev enviroment is the same as the production enviroment. Hydration issues can be missed in dev without otherwise.
TZ=UTC
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs next-logger.config.js /app/
COPY --chown=nextjs:nodejs pino-pretty-transport.js /app/

# Copy database schema
COPY --chown=nextjs:nodejs prisma ./prisma

# Create media directory
RUN mkdir -p $MEDIA_PATH
RUN chown -R nextjs:nodejs $MEDIA_PATH
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The following environment variables are used:
| GAMMA_CLIENT_ID | Gamma OAuth client ID | `id` |
| GAMMA_CLIENT_SECRET | Gamma OAuth client secret | `secret` |
| GAMMA_ROOT_URL | Gamma root URL | `https://auth.chalmers.it` |
| BASE_URL | URL that is used as a base for linking to news | `https://chalmers.it` |
| NEXTAUTH_SECRET | Secret used for signing cookies | `secret` |
| NEXTAUTH_URL | URL to the NextAuth API | `http://localhost:3000/api/auth` |
| MEDIA_PATH | Path to store media | `./media` |
Expand Down
25 changes: 25 additions & 0 deletions prisma/migrations/20241005192342_navigation_items/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "NavbarCategory" (
"id" SERIAL NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 0,
"nameSv" TEXT NOT NULL,
"nameEn" TEXT NOT NULL,
"url" TEXT NOT NULL,

CONSTRAINT "NavbarCategory_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "NavbarItem" (
"id" SERIAL NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 0,
"nameSv" TEXT NOT NULL,
"nameEn" TEXT NOT NULL,
"url" TEXT NOT NULL,
"categoryId" INTEGER NOT NULL,

CONSTRAINT "NavbarItem_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "NavbarItem" ADD CONSTRAINT "NavbarItem_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "NavbarCategory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
19 changes: 19 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,22 @@ enum SponsorType {
PARTNER
MAIN_PARTNER
}

model NavbarCategory {
id Int @id @default(autoincrement())
priority Int @default(0)
nameSv String
nameEn String
url String
NavbarItem NavbarItem[]
}

model NavbarItem {
id Int @id @default(autoincrement())
priority Int @default(0)
nameSv String
nameEn String
url String
category NavbarCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId Int
}
75 changes: 75 additions & 0 deletions src/actions/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use server';

import NavService from '@/services/navService';
import SessionService from '@/services/sessionService';

export async function addCategory(
nameEn: string,
nameSv: string,
priority?: number,
url?: string
) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.addCategory(nameEn, nameSv, priority, url);
}

export async function updateCategory(
id: number,
nameEn: string,
nameSv: string,
priority?: number,
url?: string
) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.updateCategory(id, nameEn, nameSv, priority, url);
}

export async function removeCategory(id: number) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.removeCategory(id);
}

export async function addItem(
categoryId: number,
nameEn: string,
nameSv: string,
url: string,
priority?: number
) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.addItem(categoryId, nameEn, nameSv, url, priority);
}

export async function updateItem(
id: number,
nameEn: string,
nameSv: string,
url: string,
priority?: number
) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.updateItem(id, nameEn, nameSv, url, priority);
}

export async function removeItem(id: number) {
if (!(await SessionService.isAdmin())) {
throw new Error('Unauthorized');
}

return await NavService.removeItem(id);
}
2 changes: 2 additions & 0 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ export function generateStaticParams() {
return i18nConfig.locales.map((locale) => ({ locale }));
}

export const dynamic = 'force-dynamic';
export const dynamicParams = false;
export const revalidate = false;

export default function RootLayout({
children,
Expand Down
5 changes: 5 additions & 0 deletions src/app/[locale]/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ const pages = [
path: '/settings/media',
name: 'Media',
authFunc: SessionService.isAdmin
},
{
path: '/settings/navbar',
name: 'Navigation',
authFunc: SessionService.isAdmin
}
];

Expand Down
58 changes: 58 additions & 0 deletions src/app/[locale]/settings/navbar/AddCategoryForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import { addCategory } from '@/actions/navigation';
import ActionButton from '@/components/ActionButton/ActionButton';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';

const AddCategoryForm = () => {
const router = useRouter();
const [nameEn, setNameEn] = useState('');
const [nameSv, setNameSv] = useState('');
const [url, setUrl] = useState('');
const [priority, setPriority] = useState(0);

const handleSubmit = async (e: any) => {
e.preventDefault();
toast
.promise(addCategory(nameEn, nameSv, priority, url), {
pending: 'Creating...',
success: 'Category created',
error: 'Failed to create category'
})
.then(() => router.refresh());
};

return (
<form onSubmit={handleSubmit}>
<label>Name (En)</label>
<input
type="text"
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
/>
<br />
<label>Name (Sv)</label>
<input
type="text"
value={nameSv}
onChange={(e) => setNameSv(e.target.value)}
/>
<br />
<label>URL</label>
<input type="text" value={url} onChange={(e) => setUrl(e.target.value)} />
<br />
<label>Priority</label>
<input
type="number"
value={priority}
onChange={(e) => setPriority(parseInt(e.target.value))}
/>
<br />
<ActionButton type="submit">Create</ActionButton>
</form>
);
};

export default AddCategoryForm;
58 changes: 58 additions & 0 deletions src/app/[locale]/settings/navbar/AddItemForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import { addItem } from '@/actions/navigation';
import ActionButton from '@/components/ActionButton/ActionButton';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';

const AddItemForm = ({ categoryId }: { categoryId: number }) => {
const router = useRouter();
const [nameEn, setNameEn] = useState('');
const [nameSv, setNameSv] = useState('');
const [url, setUrl] = useState('');
const [priority, setPriority] = useState(0);

const handleSubmit = async (e: any) => {
e.preventDefault();
toast
.promise(addItem(categoryId, nameEn, nameSv, url, priority), {
pending: 'Creating...',
success: 'Item created',
error: 'Failed to create item'
})
.then(() => router.refresh());
};

return (
<form onSubmit={handleSubmit}>
<label>Name (En)</label>
<input
type="text"
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
/>
<br />
<label>Name (Sv)</label>
<input
type="text"
value={nameSv}
onChange={(e) => setNameSv(e.target.value)}
/>
<br />
<label>URL</label>
<input type="text" value={url} onChange={(e) => setUrl(e.target.value)} />
<br />
<label>Priority</label>
<input
type="number"
value={priority}
onChange={(e) => setPriority(parseInt(e.target.value))}
/>
<br />
<ActionButton type="submit">Create</ActionButton>
</form>
);
};

export default AddItemForm;
74 changes: 74 additions & 0 deletions src/app/[locale]/settings/navbar/EditCategoryForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import { removeCategory, updateCategory } from '@/actions/navigation';
import ActionButton from '@/components/ActionButton/ActionButton';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';

const EditCategoryForm = ({ category }: { category: any }) => {
const router = useRouter();
const [nameEn, setNameEn] = useState(category.nameEn);
const [nameSv, setNameSv] = useState(category.nameSv);
const [url, setUrl] = useState(category.url);
const [priority, setPriority] = useState(category.priority);

const handleSubmit = async (e: any) => {
e.preventDefault();
toast
.promise(updateCategory(category.id, nameEn, nameSv, priority, url), {
pending: 'Editing...',
success: 'Category edited',
error: 'Failed to edit category'
})
.then(() => router.refresh());
};

const handleDelete = async () => {
confirm(
'Are you sure you want to delete this category? All sub-items will be deleted!'
) &&
toast
.promise(removeCategory(category.id), {
pending: 'Deleting...',
success: 'Category deleted',
error: 'Failed to delete category'
})
.then(() => router.refresh());
};

return (
<form onSubmit={handleSubmit}>
<label>Name (En)</label>
<input
type="text"
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
/>
<br />
<label>Name (Sv)</label>
<input
type="text"
value={nameSv}
onChange={(e) => setNameSv(e.target.value)}
/>
<br />
<label>URL</label>
<input type="text" value={url} onChange={(e) => setUrl(e.target.value)} />
<br />
<label>Priority</label>
<input
type="number"
value={priority}
onChange={(e) => setPriority(parseInt(e.target.value))}
/>
<br />
<ActionButton type="submit">Save</ActionButton>
<ActionButton onClick={handleDelete} type="button">
Delete
</ActionButton>
</form>
);
};

export default EditCategoryForm;
Loading
Loading