diff --git a/.env b/.env index ad22e481..35d92988 100644 --- a/.env +++ b/.env @@ -1,20 +1,20 @@ # uncomment for debug server logs # DEBUG=true +### BE URL +# nizsie povolene vars prepisuju vyssie - zakomentuj podla potreby + ## developovanie proti lokalnemu BE BE_PROTOCOL=http BE_HOSTNAME=localhost BE_PORT=8000 -BE_PREFIX=/api ## developovanie proti BE na test.strom.sk -# BE_PROTOCOL=https -# BE_HOSTNAME=test.strom.sk -# BE_PORT= -# BE_PREFIX=/api +BE_PROTOCOL=https +BE_HOSTNAME=test.strom.sk +BE_PORT= ## developovanie proti BE na strom.sk # BE_PROTOCOL=https # BE_HOSTNAME=strom.sk -# BE_PORT= -# BE_PREFIX=/api \ No newline at end of file +# BE_PORT= \ No newline at end of file diff --git a/README.md b/README.md index bdfea3d1..9d5c0e19 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,90 @@ -# Návod na spustenie +# webstrom-frontend + +- [Spustenie backendu](#spustenie-backendu) + - [Lokálny BE](#lokálny-be) +- [Spustenie frontendu](#spustenie-frontendu) +- [CSS types (deprecated)](#css-types-deprecated) +- [IDE setup](#ide-setup) ## Spustenie backendu -Na testovanie frontendu je potrebné mať prístup k API rozhraniu na backende. Návod na rozbehanie backendu sa nachádza vo [webstrom-backend](https://github.com/ZdruzenieSTROM/webstrom-backend/blob/master/README.md) repozitári. +Defaultne sú API requesty smerované na deploynutý test BE (test.strom.sk). Zmeniť to môžeš v [.env](.env). + +### Lokálny BE + +Ak potrebuješ urobiť nejaké zmeny aj na BE (a otestovať, že to s FE funguje), musíš si spustiť BE lokálne a nasmerovať naň FE. +Ak ti deployed BE stačí, pokračuj na [Spustenie frontendu](#spustenie-frontendu). + +Návod na rozbehanie backendu sa nachádza vo [webstrom-backend](https://github.com/ZdruzenieSTROM/webstrom-backend/blob/master/README.md) repozitári. Po inštalácii potrebných balíkov a vytvorení databázy spusti backend django server pomocou: -``` +```sh python manage.py runserver ``` Tento príkaz spustí server na `localhost:8000`, kde sa dá pristupovať k API a k django admin stránke. +Zakomentuj/odkomentuj riadky v [.env](.env), aby requesty smerovali na BE na `localhost:8000`. + ## Spustenie frontendu Naklonuj si projekt z GitHubu a prepni sa do priečinku projektu: -``` +```sh git clone https://github.com/ZdruzenieSTROM/webstrom-frontend cd webstrom-frontend ``` -Na nainštalovanie potrebných balíkov je potrebné mať nainštalovaný [node.js](https://nodejs.org/en/) spolu so správcom balíkov [yarn](https://classic.yarnpkg.com/en/docs/install/#windows-stable) a potrebné balíky pre projekt sa nainštalujú pomocou: +Potrebuješ: -``` +- [Node.js](https://nodejs.org/en/) - javascriptový engine + - check, či ho máš: `node -v` + - dá sa nainštalovať priamo zo stránky, ale ideálne je použiť [nvm (Node Version Manager)](https://github.com/nvm-sh/nvm) (neboj sa inštalačných inštrukcií) +- [Yarn](https://yarnpkg.com/getting-started/install) - správca JS balíkov + +Potrebné balíky pre projekt sa nainštalujú pomocou: + +```sh yarn install ``` alebo len -``` +```sh yarn ``` Development server sa spustí pomocou príkazu: -``` +```sh yarn dev ``` Tento príkaz spustí server na `localhost:3000`, ktorý reaguje na zmeny vo frontendovom kóde a automaticky sa reloaduje. -## CSS types +## CSS types (deprecated) -Na pregenerovanie CSS typov, popisujúcich typy pre `styles` z `*.module.css` do súborov `*.module.scss.d.ts` je potrebné spustiť +DEPRECATED: `.module.scss` súborov sa snažíme zbaviť. -``` +Na pregenerovanie CSS typov pre `styles` z `*.module.scss` (do súborov `*.module.scss.d.ts`) je potrebné spustiť: + +```sh yarn css-types ``` -Ak chceme aby sa tieto typy generovali automaticky počas vyvvíjania, je tu príkaz +Ak chceme aby sa tieto typy generovali automaticky počas vyvíjania, je tu príkaz: -``` +```sh yarn css-types-watch ``` -# IDE setup +## IDE setup -Používame VSCode, nainštaluj si doň [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) rozšírenie. V repe máme `.vscode` config, preto sa kód pri uložení automaticky formátuje. Rozšírenie je možné doinštalovať pomocou `Ctrl+P` a spustením: +Používame VSCode, nainštaluj si doň [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) rozšírenie (formátuje kód). V repe máme `.vscode` config, v ktorom zapíname "fix-on-save" - kód sa teda formátuje pri uložení súboru. -``` +Rozšírenie je takisto možné doinštalovať pomocou `Ctrl+P` a spustením: + +```sh ext install dbaeumer.vscode-eslint ``` diff --git a/deployment/Dockerfile b/deployment/Dockerfile index e84345b4..7e1977fd 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -3,7 +3,6 @@ FROM node:20 ARG BE_PROTOCOL ARG BE_HOSTNAME ARG BE_PORT -ARG BE_PREFIX WORKDIR /app diff --git a/deployment/compose-test.yaml b/deployment/compose-test.yaml index 062f570f..4749d249 100644 --- a/deployment/compose-test.yaml +++ b/deployment/compose-test.yaml @@ -9,7 +9,6 @@ services: - BE_PROTOCOL=http - BE_HOSTNAME=localhost - BE_PORT=8920 - - BE_PREFIX=/api image: webstrom-test-frontend @@ -18,7 +17,6 @@ services: - BE_PROTOCOL=http - BE_HOSTNAME=localhost - BE_PORT=8920 - - BE_PREFIX=/api network_mode: host diff --git a/deployment/local-mac/Dockerfile b/deployment/local-mac/Dockerfile index c866f016..71a795c3 100644 --- a/deployment/local-mac/Dockerfile +++ b/deployment/local-mac/Dockerfile @@ -3,7 +3,6 @@ FROM node:20 ARG BE_PROTOCOL ARG BE_HOSTNAME ARG BE_PORT -ARG BE_PREFIX WORKDIR /app diff --git a/deployment/local-mac/compose.yaml b/deployment/local-mac/compose.yaml index fc372bc2..dce92a4e 100644 --- a/deployment/local-mac/compose.yaml +++ b/deployment/local-mac/compose.yaml @@ -7,7 +7,6 @@ services: - BE_PROTOCOL=http - BE_HOSTNAME=host.docker.internal - BE_PORT=8000 - - BE_PREFIX=/api image: webstrom-test-frontend @@ -16,7 +15,6 @@ services: - BE_PROTOCOL=http - BE_HOSTNAME=host.docker.internal - BE_PORT=8000 - - BE_PREFIX=/api ports: - '3000:3000' diff --git a/navod.md b/navod.md deleted file mode 100644 index 67e344b2..00000000 --- a/navod.md +++ /dev/null @@ -1,35 +0,0 @@ -# Návod na spustenie - -## Spustenie backendu - -Na testovanie frontendu je potrebné mať prístup k API rozhraniu na backende. Návod na rozbehanie backendu sa nachádza vo [webstrom-backend](https://github.com/ZdruzenieSTROM/webstrom-backend/blob/master/navod.md) repozitári. - -Po inštalácii potrebných balíkov a vytvorení databázy spusti backend django server pomocou: - -``` -python manage.py runserver -``` -Tento príkaz spustí server na `localhost:8000`, kde sa dá pristupovať k API a k django admin stránke. - -## Spustenie frontendu - -Naklonuj si projekt z GitHubu a prepni sa do priečinku projektu: - -``` -git clone https://github.com/ZdruzenieSTROM/webstrom-frontend -cd webstrom-frontend -``` - -Na nainštalovanie potrebných balíkov je potrebné mať nainštalovaný [node.js](https://nodejs.org/en/) spolu so správcom balíkov [yarn](https://classic.yarnpkg.com/en/docs/install/#windows-stable) a potrebné balíky pre projekt sa nainštalujú pomocou: -``` -yarn install -```` -alebo len -``` -yarn -``` -Development server sa spustí pomocou príkazu: -``` -yarn start -``` -Tento príkaz spustí server na `localhost:3000`, ktorý reaguje na zmeny vo frontendovom kóde a automaticky sa reloaduje. \ No newline at end of file diff --git a/src/api/apiAxios.ts b/src/api/apiAxios.ts index b70982d0..c2f8589d 100644 --- a/src/api/apiAxios.ts +++ b/src/api/apiAxios.ts @@ -2,11 +2,16 @@ import axios from 'axios' import {apiInterceptor} from '@/api/apiInterceptor' import {debugServer} from '@/utils/debugServer' -import {getInternalBeServerUrl} from '@/utils/urlBase' +import {getBackendServerUrl} from '@/utils/urlBase' export const newApiAxios = (base: 'server' | 'client') => { const instance = axios.create({ - baseURL: base === 'server' ? getInternalBeServerUrl() : '/api', + // axios requesty mozu byt z FE next serveru alebo z browsru. + // - server vola BE URL (podla env vars) priamo + // - browser vola lokalnu /api URL + // - na deployed verzii (test.strom.sk, strom.sk) to chyti nginx a posle na deployed BE + // - na localhoste to chyti next middleware, kde to rewritneme na BE URL (podla env vars) + baseURL: base === 'server' ? `${getBackendServerUrl()}/api` : '/api', // auth pozostava z comba: // 1. `sessionid` httpOnly cookie - nastavuje a maze su server pri login/logout // 2. CSRF hlavicka - server nastavuje cookie, ktorej hodnotu treba vlozit do hlavicky. axios riesi automaticky podla tohto configu @@ -24,6 +29,8 @@ export const newApiAxios = (base: 'server' | 'client') => { debugServer('[SERVER API]', method?.toUpperCase(), url && baseURL ? new URL(url, baseURL).href : url) + // server-side requesty z deployed FE na deployed BE potrebuju tieto hlavicky + // TODO: ked pojdeme do produkcie, asi bude treba riesit nejakym env varom config.headers['X-Forwarded-Host'] = 'test.strom.sk' config.headers['X-Forwarded-Proto'] = 'https' diff --git a/src/api/apiInterceptor.ts b/src/api/apiInterceptor.ts index 6a3b5289..1607d232 100644 --- a/src/api/apiInterceptor.ts +++ b/src/api/apiInterceptor.ts @@ -1,6 +1,6 @@ import axios from 'axios' -import {addApiTrailingSlash} from '@/utils/addApiTrailingSlash' +import {addTrailingSlash} from '@/utils/trailingSlash' type RequestInterceptor = Parameters[0] @@ -8,7 +8,7 @@ export const apiInterceptor: RequestInterceptor = (config) => { if (config.url) { const [pathname, query] = config.url.split('?') - const newPathname = addApiTrailingSlash(pathname) + const newPathname = addTrailingSlash(pathname) config.url = `${newPathname}${query ? `?${query}` : ''}` } diff --git a/src/components/Archive/Archive.tsx b/src/components/Archive/Archive.tsx index 872a74cb..3adcdc22 100644 --- a/src/components/Archive/Archive.tsx +++ b/src/components/Archive/Archive.tsx @@ -23,7 +23,7 @@ const PublicationButton: FC<{ publication: Publication }> = ({publication}) => { return ( - + {publication.name} ) diff --git a/src/components/Clickable/Link.tsx b/src/components/Clickable/Link.tsx index f9daaf27..a136b5a0 100644 --- a/src/components/Clickable/Link.tsx +++ b/src/components/Clickable/Link.tsx @@ -13,9 +13,27 @@ type LinkProps = { active?: boolean sx?: SxProps textSx?: SxProps -} & Pick, 'target'> +} & Pick, 'target' | 'prefetch'> + +export const Link: FC = ({ + children, + href, + disabled, + target, + variant, + invertColors, + active, + sx, + textSx, + prefetch: overridePrefetch, +}) => { + // https://nextjs.org/docs/pages/api-reference/components/link#prefetch + // by default, next.js prefetchuje stranky vsetkych linkov vo viewporte. pre media PDFka je to zbytocne heavy. + // inak aj pri `false` sa stale prefetchne pri hoveri, co je acceptable. + const isMedia = href?.startsWith('/media') + const defaultPrefetch = !isMedia + const prefetch = overridePrefetch ?? defaultPrefetch -export const Link: FC = ({children, href, disabled, target, variant, invertColors, active, sx, textSx}) => { if (disabled) { return ( = ({children, href, disabled, target, variant, component={NextLink} href={href ?? ''} target={target} + prefetch={prefetch} sx={{ ...getButtonWrapperSx({invertColors, disabled, active}), ...sx, diff --git a/src/components/CompetitionPage/CompetitionPage.tsx b/src/components/CompetitionPage/CompetitionPage.tsx index 1b1bc6ee..3d8f6f81 100644 --- a/src/components/CompetitionPage/CompetitionPage.tsx +++ b/src/components/CompetitionPage/CompetitionPage.tsx @@ -94,19 +94,19 @@ export const CompetitionPage: FC = ({ {results && ( - + {PublicationTypes.RESULTS.display_name} )} {solutions ? ( - + {PublicationTypes.SOLUTIONS.display_name} ) : ( problems && ( - + {PublicationTypes.PROBLEMS.display_name} ) diff --git a/src/components/CompetitionPage/UpcomingOrCurrentEventInfo.tsx b/src/components/CompetitionPage/UpcomingOrCurrentEventInfo.tsx index 00c44b08..4aa226c6 100644 --- a/src/components/CompetitionPage/UpcomingOrCurrentEventInfo.tsx +++ b/src/components/CompetitionPage/UpcomingOrCurrentEventInfo.tsx @@ -47,7 +47,7 @@ export const UpcomingOrCurrentEventInfo: FC<{event: Event; name: string; shortNa {invitationFile && ( - + Pozvánka )} diff --git a/src/components/PageLayout/MenuMain/MenuMain.tsx b/src/components/PageLayout/MenuMain/MenuMain.tsx index 32858043..e63fcc52 100644 --- a/src/components/PageLayout/MenuMain/MenuMain.tsx +++ b/src/components/PageLayout/MenuMain/MenuMain.tsx @@ -17,7 +17,7 @@ import {BottomButtons} from './BottomButtons' export const MenuMain: FC = () => { const {seminar, seminarId} = useSeminarInfo() - const {hasPermissions} = useHasPermissions() + const {hasPermissions, isSuperuser} = useHasPermissions() const [isVisible, setIsVisible] = useState(false) const toggleMenu = () => setIsVisible((currentIsVisible) => !currentIsVisible) @@ -125,6 +125,7 @@ export const MenuMain: FC = () => { + {isSuperuser && } )} diff --git a/src/components/PublicationUploader/PublicationUploader.tsx b/src/components/PublicationUploader/PublicationUploader.tsx index 3fea31ff..45a8b2ad 100644 --- a/src/components/PublicationUploader/PublicationUploader.tsx +++ b/src/components/PublicationUploader/PublicationUploader.tsx @@ -32,7 +32,7 @@ export const PublicationUploader: FC = ({semesterId, o {order}. Časopis: {publication && ( - + {publication.name} )} diff --git a/src/middleware.ts b/src/middleware.ts index 14af30b6..1c26fc88 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,21 +1,40 @@ import {NextRequest, NextResponse} from 'next/server' -import {apiMiddleware} from './middleware/apiMiddleware' +import {backendRewriteMiddleware} from './middleware/backendRewriteMiddleware' +import {removeTrailingSlash} from './utils/trailingSlash' export function middleware(req: NextRequest) { const url = req.nextUrl + // - na deployed serveri tieto lokalne routy chyti nginx proxy a posle ich na BE, + // do tohto middlewaru sa to nedostane. + // - na localhoste tieto routy chyti next.js a posle ich do tohto middlewaru. + // simulujeme nginx podla viacmenej podla tohto: + // https://github.com/ZdruzenieSTROM/webstrom-backend/pull/491#discussion_r1893181775 if (url.pathname.startsWith('/api')) { - return apiMiddleware(req) + return backendRewriteMiddleware({req, trailingSlash: true}) + } + // casopisy, riesenia, opravene riesenia + if (url.pathname.startsWith('/media')) { + return backendRewriteMiddleware({req, trailingSlash: false}) + } + // napr. http://localhost:3000/django-admin + if (url.pathname.startsWith('/django-admin')) { + return backendRewriteMiddleware({req, trailingSlash: true}) + } + // napr. `/django-admin` fetchuje CSSka zo `/static` + if (url.pathname.startsWith('/static')) { + return backendRewriteMiddleware({req, trailingSlash: false}) } // https://nextjs.org/docs/app/building-your-application/routing/middleware#advanced-middleware-flags // odstran trailing slash - default next.js spravanie, ale vypli sme ho v next.config.ts pomocou // `skipTrailingSlashRedirect: true`, aby sme dovolili (a v axiose a middlewari vyssie aj forcli) // trailing slash pre BE Django - // pre root route `/` to nemozeme spravit (infinite redirect) ¯\_(ツ)_/¯ - if (url.pathname.endsWith('/') && url.pathname !== '/') { - const newPathname = url.pathname.slice(0, -1) + const newPathname = removeTrailingSlash(url.pathname) + + // redirect ak sa da odstranit trailing slash + if (newPathname !== url.pathname) { const newUrl = new URL(`${newPathname}${url.search}`, url) return NextResponse.redirect(newUrl, 308) diff --git a/src/middleware/apiMiddleware.ts b/src/middleware/apiMiddleware.ts deleted file mode 100644 index 81aa33e9..00000000 --- a/src/middleware/apiMiddleware.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {NextRequest, NextResponse} from 'next/server' - -import {addApiTrailingSlash} from '@/utils/addApiTrailingSlash' -import {getInternalBeServerUrl} from '@/utils/urlBase' - -export const apiMiddleware = (req: NextRequest) => { - const {method, nextUrl} = req - const {pathname, search, href} = nextUrl - - let newPathname = pathname - - // nahrad prefix "/api" za iny prefix (na lokalny BE "", na deployed BE "/api") - newPathname = newPathname.replace(/^\/api/, process.env.BE_PREFIX ?? '') - - newPathname = addApiTrailingSlash(newPathname) - - const newUrl = new URL(`${newPathname}${search}`, getInternalBeServerUrl()) - - // eslint-disable-next-line no-console - if (process.env.DEBUG === 'true') console.log('[MIDDLEWARE]', method, href, '->', newUrl.href) - - return NextResponse.rewrite(newUrl) -} diff --git a/src/middleware/backendRewriteMiddleware.ts b/src/middleware/backendRewriteMiddleware.ts new file mode 100644 index 00000000..5c50b85b --- /dev/null +++ b/src/middleware/backendRewriteMiddleware.ts @@ -0,0 +1,20 @@ +import {NextRequest, NextResponse} from 'next/server' + +import {addTrailingSlash} from '@/utils/trailingSlash' +import {getBackendServerUrl} from '@/utils/urlBase' + +export const backendRewriteMiddleware = ({req, trailingSlash}: {req: NextRequest; trailingSlash: boolean}) => { + const {method, nextUrl} = req + const {pathname, search, href} = nextUrl + + let newPathname = pathname + + if (trailingSlash) newPathname = addTrailingSlash(newPathname) + + const newUrl = new URL(`${newPathname}${search}`, getBackendServerUrl()) + + // eslint-disable-next-line no-console + if (process.env.DEBUG === 'true') console.log('[MIDDLEWARE]', method, href, '->', newUrl.href) + + return NextResponse.rewrite(newUrl) +} diff --git a/src/types/api/personal.ts b/src/types/api/personal.ts index 7d6adc2a..b9fd1e29 100644 --- a/src/types/api/personal.ts +++ b/src/types/api/personal.ts @@ -78,6 +78,7 @@ export interface Profile { export interface MyPermissions { is_staff: boolean + is_superuser: boolean competition_permissions: number[] } diff --git a/src/utils/addApiTrailingSlash.ts b/src/utils/addApiTrailingSlash.ts deleted file mode 100644 index d9709b2a..00000000 --- a/src/utils/addApiTrailingSlash.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * BE Django server ocakava trailing slash (aj pred query params). - * - * @param pathname local URL without query params. e.g. `/api/cms/post` - * @returns local URL with trailing slash. e.g. `/api/cms/post/` - * */ -export const addApiTrailingSlash = (pathname: string) => { - if (!pathname.endsWith('/')) return `${pathname}/` - - return pathname -} diff --git a/src/utils/trailingSlash.ts b/src/utils/trailingSlash.ts new file mode 100644 index 00000000..622fd066 --- /dev/null +++ b/src/utils/trailingSlash.ts @@ -0,0 +1,20 @@ +/** + * BE Django server ocakava trailing slash (aj pred query params). + * + * @param pathname local URL without query params. e.g. `/cms/post` + * @returns local URL with trailing slash. e.g. `/cms/post/` + * */ +export const addTrailingSlash = (pathname: string) => { + if (!pathname.endsWith('/')) return `${pathname}/` + + return pathname +} + +export const removeTrailingSlash = (pathname: string) => { + // pre root route `/` nemozeme odstranit slash (infinite redirect) + if (pathname === '/') return pathname + + if (pathname.endsWith('/')) return pathname.slice(0, -1) + + return pathname +} diff --git a/src/utils/urlBase.ts b/src/utils/urlBase.ts index ea141a6c..e6225286 100644 --- a/src/utils/urlBase.ts +++ b/src/utils/urlBase.ts @@ -1,10 +1,6 @@ -export const composeUrlBase = (protocol: string, hostname: string, port: string, prefix: string) => - `${protocol}://${hostname}${port ? `:${port}` : ''}${prefix}` +export const composeUrlBase = (protocol: string, hostname: string, port: string) => + `${protocol}://${hostname}${port ? `:${port}` : ''}` -export const getInternalBeServerUrl = () => - composeUrlBase( - process.env.BE_PROTOCOL as string, - process.env.BE_HOSTNAME as string, - process.env.BE_PORT as string, - process.env.BE_PREFIX as string, - ) +/** Server-side funkcia - tieto env vars frontend nevidi. */ +export const getBackendServerUrl = () => + composeUrlBase(process.env.BE_PROTOCOL as string, process.env.BE_HOSTNAME as string, process.env.BE_PORT as string) diff --git a/src/utils/useHasPermissions.ts b/src/utils/useHasPermissions.ts index aeadebd6..940b7a9d 100644 --- a/src/utils/useHasPermissions.ts +++ b/src/utils/useHasPermissions.ts @@ -16,10 +16,11 @@ export const useHasPermissions = () => { }) const permissions = data?.data.competition_permissions + const isSuperuser = data?.data.is_superuser ?? false const {seminarId} = useSeminarInfo() const hasPermissions = !permissions ? false : permissions.includes(seminarId) - return {hasPermissions, permissionsIsLoading} + return {hasPermissions, isSuperuser, permissionsIsLoading} }