Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc5a7a7b6
|
||
|
|
864aef1b7b
|
||
|
|
50bdf6503e
|
||
|
|
b4bb073d87
|
||
|
|
8e7f4b7659
|
||
|
|
b00debdbb0
|
||
|
|
bd6012b0ab
|
||
|
|
d313f903ee
|
||
|
|
d9155dec10
|
||
|
|
95f75d9bb1
|
||
|
|
d9a6019c7c
|
||
|
|
ae59bfeb5f
|
||
|
|
171a3053f3 | ||
|
|
ee32c1723b
|
||
|
|
e2d2836e69 | ||
|
|
e7d7e29edf
|
||
|
|
e07e6ebcd0
|
||
|
|
0a3894e14d
|
||
|
|
626d749611
|
||
|
|
cf84a5086e
|
||
|
|
f2d7b15c4e
|
||
|
|
5bd7321558 | ||
|
|
400af2fb31 | ||
|
|
c7185a79f3 | ||
|
|
2c6e4c2412
|
||
|
|
31a4b1cf81
|
||
|
|
ef764a4b8d
|
||
|
|
d21053b544
|
||
|
|
828468e70e
|
||
|
|
3d798a8913
|
||
|
|
7d17abc2c5
|
||
|
|
dff4669321
|
||
|
|
dca3c08ee1
|
||
|
|
42c75194ed
|
||
|
|
ead41eeaea
|
||
|
|
d20022899a
|
||
|
|
e0735a5c97
|
||
|
|
cb7cdb8128
|
||
|
|
36ea11441f
|
||
|
|
fda4846200
|
||
|
|
91dc9542e4
|
||
|
|
0e65652e76
|
||
|
|
8a869a10eb
|
||
|
|
c0200f67f2
|
||
|
|
26e2e75765
|
||
|
|
d5131824d9
|
||
|
|
9394f1f7b4 | ||
|
|
0f5c7306d3 | ||
|
|
9f7dacfb21 | ||
|
|
ca81402a45 | ||
|
|
39dfeb21a3 | ||
|
|
3184215135 | ||
|
|
75c59eb8bb | ||
|
|
1e2444150b | ||
|
|
51a58f5c55 | ||
|
|
9ff744e5b1 | ||
|
|
d28db43afd | ||
|
|
d7ad18c60f | ||
|
|
6dacc4e5b5 | ||
|
|
568b0524e0 | ||
|
|
1c7ca69a81 | ||
|
|
89d0e6ab9c | ||
|
|
c29e9d8f57 | ||
|
|
aec4a9406e | ||
|
|
224847b1a7 | ||
|
|
dc5ac4b2c7 | ||
|
|
232f69472c | ||
|
|
0a62a61ccf | ||
|
|
2857cc7048 | ||
|
|
37c9618ad6 | ||
|
|
a9c250063c | ||
|
|
e2367eb29e | ||
|
|
8097ea89cd | ||
|
|
3d5c9d4bd9 | ||
|
|
fd5d8d1cf1 | ||
|
|
13c0eab15c | ||
|
|
88df17818c | ||
|
|
269c041a7b | ||
|
|
6ebe7565c7 | ||
|
|
bb9bf87bf5 | ||
|
|
83d5088e92 | ||
|
|
5b7d00cee3 | ||
|
|
53952c614c | ||
|
|
a33ffce59a | ||
|
|
11b872dbb8 | ||
|
|
f72f351618 | ||
|
|
33aab96bc7 | ||
|
|
01e57edc41 |
No files matched your search
@@ -0,0 +1 @@
|
||||
* @LBlend @centauridesigns
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
run_pull:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
install -m 600 -D /dev/null ~/.ssh/id_ed25519
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519 # This should be redundant but seems to do the trick.
|
||||
ssh-keyscan -H ${{ vars.SSH_HOST }} > ~/.ssh/known_hosts
|
||||
|
||||
- name: Connect, pull, build and deploy
|
||||
run: ssh ${{ secrets.SSH_USER }}@${{ vars.SSH_HOST }} "cd ${{ secrets.WORK_DIR }} && git checkout ${{ vars.MAIN_BRANCH }} && git pull && docker compose down && docker compose up -d --build && exit"
|
||||
|
||||
- name: Clean up
|
||||
run: rm -rf ~/.ssh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -1,69 +1,41 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variable files
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# Yarn
|
||||
yarn-error.log
|
||||
.pnp/
|
||||
.pnp.js
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
- Sørg for å formatere koden med Prettier
|
||||
|
||||
- Sjekk om nettsiden bygges uten problemer (`npm run build`)
|
||||
|
||||
- Commit messages samt issues skal helst være skrevet på engelsk
|
||||
|
||||
- Kode skal skrives på engelsk. Det vil si variabelnavn osv. Tekst som vises til bruker skal være på norsk
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -1,38 +1,51 @@
|
||||
# uiogaming.no
|
||||
|
||||
Dette er repoet for UiO Gaming sin nettside. Nettsiden består av tre deler hvorav to av disse er i dette repoet. Her finner du frontend for nettsiden samt et webhook API. Dette APIet brukes til å trigge bygging av nettsiden når innhold endrer seg i CMS, backenden. Repoet for dette CMSet er [her](https://github.com/UiO-Gaming/uiogaming.no-backend). Nettsiden er bygget med GatsbyJS.
|
||||
<img width="1280" height="720" alt="image" src="https://github.com/user-attachments/assets/574daf4d-085c-4df0-ab86-7f843a1a403c" />
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
Dette er repoet for UiO Gaming sin nettside. Nettsiden består av to deler. Her finner du frontend for nettsiden. Repoet for CMSet er [her](https://github.com/UiO-Gaming/uiogaming.no-backend).
|
||||
|
||||
## Oppsett
|
||||
|
||||
### Frontend
|
||||
1. Lag konfigurasjonsfil
|
||||
|
||||
1. Installer avhengigheter
|
||||
I repoet er det en fil som heter [env.example](env.example). Kopier denne og sett navnet på den nye filen til `.env`.
|
||||
|
||||
Fyll så ut verdiene.
|
||||
|
||||
2. Installer avhengigheter
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Kjør nettsiden
|
||||
3. Bygg nettsiden
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Kjør nettsiden
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Om du skal utvikle kan du kjøre følgende istedenfor steg 3 og 4
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Webhook API
|
||||
|
||||
Om du vil at innhold skal oppdatere seg når innholdet endres i backend må du kjøre dette APIet. Dette gjelder bare om du bruker en produksjonsversjon av nettsiden (`yarn serve`)
|
||||
Om du vil at innhold skal oppdatere seg når innholdet endres i backend må du kjøre dette APIet. Dette gjelder bare om du kjører en produksjonsversjon av nettsiden
|
||||
|
||||
```bash
|
||||
cd webhook-api
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
Du må spesifisere URL for webhook i Sanity sitt dashbord. URLen dette webhooket skal peke på er `https://uiogaming.no/api/revalidate` med _POST_ forespørselstype.
|
||||
|
||||
Du må også spesifisere URL for webhook i Sanity sitt dashbord. Du kan lese mer om det [her](https://www.sanity.io/docs/webhooks). Denne URLen skal peke på APIet ditt.
|
||||
|
||||
### Docker
|
||||
|
||||
Docker er for øyeblikket ikke støttet, men vil støttes fremtiden.
|
||||
Du kan lese mer om det [her](https://www.sanity.io/docs/webhooks).
|
||||
|
||||
## Vil du bidra?
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
services:
|
||||
uiogaming.no:
|
||||
container_name: uiogaming.no
|
||||
build: .
|
||||
ports:
|
||||
- 9000:9000
|
||||
@@ -0,0 +1,7 @@
|
||||
NEXT_PUBLIC_SANITY_PROJECT_ID=mmqlu667
|
||||
NEXT_PUBLIC_SANITY_DATASET=production
|
||||
|
||||
SANITY_REVALIDATE_SECRET=your-super-secret-webhook-token-here
|
||||
|
||||
NEXT_PUBLIC_UMAMI_SRC=https://umami.uiogaming.no/script.js
|
||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID=1e985053-9158-4d62-be07-0fe83a9dce6e
|
||||
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,50 +0,0 @@
|
||||
module.exports = {
|
||||
siteMetadata: {
|
||||
title: `UiO Gaming`,
|
||||
description: `En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!`,
|
||||
author: `@uiogaming`,
|
||||
url: `https://uiogaming.no`,
|
||||
},
|
||||
plugins: [
|
||||
`gatsby-plugin-react-helmet`,
|
||||
{
|
||||
resolve: "gatsby-plugin-manifest",
|
||||
options: {
|
||||
icon: "src/images/logo.jpg",
|
||||
},
|
||||
},
|
||||
`gatsby-plugin-image`,
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
name: `images`,
|
||||
path: `${__dirname}/src/images`,
|
||||
},
|
||||
},
|
||||
`gatsby-transformer-sharp`,
|
||||
`gatsby-plugin-sharp`,
|
||||
{
|
||||
resolve: `gatsby-source-sanity`,
|
||||
options: {
|
||||
projectId: `mmqlu667`,
|
||||
dataset: `production`,
|
||||
watchMode: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-manifest`,
|
||||
options: {
|
||||
name: `UiO Gaming`,
|
||||
short_name: `uiogaming.no`,
|
||||
start_url: `/`,
|
||||
background_color: `#171520`,
|
||||
theme_color: `#E2231A`,
|
||||
display: `minimal-ui`,
|
||||
icon: `src/images/logo.jpg`, // This path is relative to the root of the site.
|
||||
},
|
||||
},
|
||||
// this (optional) plugin enables Progressive Web App + Offline functionality
|
||||
// To learn more, visit: https://gatsby.dev/offline
|
||||
// `gatsby-plugin-offline`,
|
||||
],
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
const path = require("path")
|
||||
|
||||
module.exports.createPages = async ({ graphql, actions }) => {
|
||||
const { createPage } = actions
|
||||
const blogTemplate = path.resolve("./src/pages/blogPost.js")
|
||||
const eventTemplate = path.resolve("./src/pages/eventPost.js")
|
||||
const result = await graphql(`
|
||||
query {
|
||||
allSanityPost {
|
||||
nodes {
|
||||
_id
|
||||
slug {
|
||||
current
|
||||
}
|
||||
}
|
||||
}
|
||||
allSanityEvent {
|
||||
nodes {
|
||||
_id
|
||||
slug {
|
||||
current
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const posts = result.data.allSanityPost.nodes
|
||||
|
||||
posts.forEach(post => {
|
||||
const path = `/blog/${post.slug.current}`
|
||||
createPage({
|
||||
path,
|
||||
component: blogTemplate,
|
||||
context: {
|
||||
id: post._id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const events = result.data.allSanityEvent.nodes
|
||||
|
||||
events.forEach(event => {
|
||||
const path = `/event/${event.slug.current}`
|
||||
createPage({
|
||||
path,
|
||||
component: eventTemplate,
|
||||
context: {
|
||||
id: event._id,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module "*.module.css" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export = classes
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"common": {
|
||||
"photo": "Photo",
|
||||
"join": "Become a member",
|
||||
"statutes": "Statutes",
|
||||
"complaint": "Complaint",
|
||||
"documents": "Documents"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "back",
|
||||
"flagImageAlt": "Norwegian flag"
|
||||
},
|
||||
"404": {
|
||||
"title": "404: Page not found",
|
||||
"text": "Our journey ended here. Please don't leave us :(",
|
||||
"back": "Back to safe grounds"
|
||||
},
|
||||
"hero": {
|
||||
"text": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO"
|
||||
},
|
||||
"about": {
|
||||
"title": "Who are we?",
|
||||
"imageAlt": "2 members playing together",
|
||||
"text": "UiO Gaming is a student organisation with a primary focus on creating an inclusive environment around gaming and e-sports. We wish to establish a community where it’s easy to make new friends and cultivate your gaming interests. Each week we host both casual and more intense gamenights, as well as physical events such as board game nights and other social gatherings. Our main channel is a Discord server where you can meet new people with common interests and play games with other students.",
|
||||
"interested": "Interested?",
|
||||
"other": "Other"
|
||||
},
|
||||
"events": {
|
||||
"title": "Upcoming Events",
|
||||
"imageAlt": "Members playing board games",
|
||||
"readMore": "Read more",
|
||||
"noEvents": "No upcoming events"
|
||||
},
|
||||
"office": {
|
||||
"title": "The Office",
|
||||
"text": "We have an office at Chateau Neuf where you can use our gear and hang out with other members! We've got all the latest and greatest consoles, 6 high-end PCs as well as a plethora of board games.",
|
||||
"tip": "Tip: You can look around the office by clicking and dragging the image below 👀"
|
||||
},
|
||||
"boardMembers": {
|
||||
"title": "The Board",
|
||||
"imageAlt": "Potrait of board member",
|
||||
"noContact": "No contact info"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Find us",
|
||||
"email": "E-mail"
|
||||
},
|
||||
"footer": {
|
||||
"sourceCode": "Source code",
|
||||
"admin": "Admin page",
|
||||
"orgNum": "Org. No."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"common": {
|
||||
"photo": "Foto",
|
||||
"join": "Bli medlem",
|
||||
"statutes": "Vedtekter",
|
||||
"complaint": "Klage",
|
||||
"documents": "Dokumenter"
|
||||
},
|
||||
"buttons": {
|
||||
"back": "tilbake",
|
||||
"flagImageAlt": "Britisk flagg"
|
||||
},
|
||||
"404": {
|
||||
"title": "404: Siden finnes ikke",
|
||||
"text": "Du har nådd en blindvei, men ikke på Blindern",
|
||||
"back": "Tilbake til hjemmesiden"
|
||||
},
|
||||
"hero": {
|
||||
"text": "En hobbyforening som ønsker å skape et sosialt miljø rundt gaming og e-sport på UiO"
|
||||
},
|
||||
"about": {
|
||||
"title": "Hvem er vi?",
|
||||
"imageAlt": "2 medlemmer som spiller sammen",
|
||||
"text": "UiO Gaming er en forening med hovedfokus på å danne et inkluderende fellesskap rundt videospill og e-sport. Hos oss er det enkelt å skaffe seg venner, ettersom alle har felles interesser. Vi arrangerer ukentlige spillkvelder og fysiske sosiale arrangementer, som for eksempel brettspillkvelder og sosiale sammenkomster. Vår hovedkanal er en aktiv Discord-server hvor man kan møte nye folk med felles interesser og game med andre studenter.",
|
||||
"interested": "Interessert?",
|
||||
"other": "Annet"
|
||||
},
|
||||
"events": {
|
||||
"title": "Hva skjer?",
|
||||
"imageAlt": "Medlemmer som spiller brettspill",
|
||||
"readMore": "Les mer",
|
||||
"noEvents": "Ingen kommende arrangementer"
|
||||
},
|
||||
"office": {
|
||||
"title": "Kontoret",
|
||||
"text": "Vi har et foreningskontor på Chateau Neuf der man kan bruke utstyret vårt og henge med andre medlemmer! Vi har blant annet alle de nyeste konsollene, 6 gaming-PCer samt et bredt utvalg av brettspill.",
|
||||
"tip": "Tips: Du kan kikke rundt i kontoret ved å klikke og dra på bildet nedenfor 👀"
|
||||
},
|
||||
"boardMembers": {
|
||||
"title": "Styret",
|
||||
"imageAlt": "Bilde av styremedlem",
|
||||
"noContact": "Ingen kontaktinfo"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Finn oss",
|
||||
"email": "E-post"
|
||||
},
|
||||
"footer": {
|
||||
"sourceCode": "Kildekode",
|
||||
"admin": "Adminområdet",
|
||||
"orgNum": "Org.Nr."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NextConfig } from "next"
|
||||
import createNextIntlPlugin from "next-intl/plugin"
|
||||
|
||||
const withNextIntl = createNextIntlPlugin()
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.sanity.io",
|
||||
port: "",
|
||||
pathname: "/images/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default withNextIntl(nextConfig)
|
||||
@@ -1,38 +1,39 @@
|
||||
{
|
||||
"name": "uiogaming.no",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "UiO Gaming Website",
|
||||
"version": "0.1.2",
|
||||
"author": "LBlend <lblend@protonmail.com",
|
||||
"author": "Leander Furumo <leander@furumo.eu>",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start -p 9000",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@sanity/block-content-to-react": "^3.0.0",
|
||||
"gatsby": "^3.4.1",
|
||||
"gatsby-plugin-image": "^1.4.0",
|
||||
"gatsby-plugin-manifest": "^3.4.0",
|
||||
"gatsby-plugin-react-helmet": "^4.4.0",
|
||||
"gatsby-plugin-sharp": "^3.7.1",
|
||||
"gatsby-source-filesystem": "^3.7.1",
|
||||
"gatsby-source-sanity": "^7.0.4",
|
||||
"gatsby-transformer-sharp": "^3.7.1",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-pannellum": "^0.2.14"
|
||||
"@portabletext/react": "^3.2.1",
|
||||
"@sanity/client": "^7.8.1",
|
||||
"@sanity/image-url": "^1.1.0",
|
||||
"country-flag-icons": "^1.5.19",
|
||||
"install": "^0.13.0",
|
||||
"next": "15.4.4",
|
||||
"next-intl": "^4.3.4",
|
||||
"npm": "^11.5.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-photo-sphere-viewer": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
|
||||
"start": "npm run develop",
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 MiB After Width: | Height: | Size: 5.7 MiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 18 MiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 696 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -0,0 +1,43 @@
|
||||
import { revalidateTag } from "next/cache"
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const secret = request.nextUrl.searchParams.get("secret")
|
||||
|
||||
if (secret !== process.env.SANITY_REVALIDATE_SECRET) {
|
||||
return NextResponse.json({ message: "Invalid secret" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { _type, slug } = body
|
||||
|
||||
console.log(
|
||||
`Revalidating content: ${_type}${slug ? ` (${slug.current})` : ""}`
|
||||
)
|
||||
|
||||
if (_type === "post") {
|
||||
revalidateTag("blog-posts")
|
||||
if (slug?.current) {
|
||||
revalidateTag(`post-${slug.current}`)
|
||||
}
|
||||
} else if (_type === "event") {
|
||||
revalidateTag("events")
|
||||
if (slug?.current) {
|
||||
revalidateTag(`event-${slug.current}`)
|
||||
}
|
||||
}
|
||||
|
||||
revalidateTag("home")
|
||||
|
||||
return NextResponse.json({
|
||||
revalidated: true,
|
||||
now: Date.now(),
|
||||
type: _type,
|
||||
slug: slug?.current,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Error revalidating:", err)
|
||||
return NextResponse.json({ message: "Error revalidating" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
.article {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.header {
|
||||
display: grid;
|
||||
@@ -21,6 +22,9 @@
|
||||
.headerImage {
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
place-self: center;
|
||||
@@ -53,6 +57,7 @@
|
||||
display: grid;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
@@ -60,11 +65,6 @@
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Block content styling */
|
||||
/* .content {
|
||||
padding-inline-start: 1rem;
|
||||
border-inline-start: 8px solid var(--main-color);
|
||||
} */
|
||||
.content blockquote {
|
||||
color: rgb(225, 225, 225);
|
||||
font-weight: 100;
|
||||
@@ -77,18 +77,9 @@
|
||||
.content h2,
|
||||
.content h3,
|
||||
.content h4 {
|
||||
margin-top: 3rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* .content blockquote::before {
|
||||
content: '"';
|
||||
font-size: 3rem;
|
||||
}
|
||||
.content blockquote::after {
|
||||
content: '"';
|
||||
font-size: 3rem;
|
||||
} */
|
||||
|
||||
@media (min-width: 60em) {
|
||||
.content {
|
||||
padding: 0;
|
||||
@@ -0,0 +1,161 @@
|
||||
import Layout from "@/app/layout"
|
||||
|
||||
import Footer from "@/components/page-sections/footer"
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
import Back from "@/components/ui/back"
|
||||
|
||||
import { urlFor } from "@/lib/imageUrlBuilder"
|
||||
import { getBlogPost } from "@/lib/sanity"
|
||||
|
||||
import { PortableText, PortableTextComponents } from "@portabletext/react"
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { getLocale } from "next-intl/server"
|
||||
import Image from "next/image"
|
||||
|
||||
import * as styles from "./page.module.css"
|
||||
|
||||
interface BlogPageParams {
|
||||
params: Promise<{
|
||||
slug: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface ImageValue {
|
||||
asset?: {
|
||||
_id: string
|
||||
url: string
|
||||
}
|
||||
alt?: string
|
||||
caption?: string
|
||||
}
|
||||
|
||||
interface LinkValue {
|
||||
href: string
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: BlogPageParams): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const data = await getBlogPost(slug)
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Blog Post Not Found | UiO Gaming",
|
||||
}
|
||||
}
|
||||
|
||||
return generateSeoMetadata({
|
||||
title: data.title,
|
||||
description: data.excerpt,
|
||||
imageURL: data.mainImage?.asset?.url,
|
||||
author: data.author?.name,
|
||||
})
|
||||
}
|
||||
|
||||
export function generateViewport(): Viewport {
|
||||
return generateSeoViewport()
|
||||
}
|
||||
|
||||
const components: PortableTextComponents = {
|
||||
types: {
|
||||
image: ({ value }: { value: ImageValue }) => {
|
||||
if (!value?.asset) return null
|
||||
return (
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
src={urlFor(value).url()}
|
||||
alt={value.alt || "Blog image"}
|
||||
width={800}
|
||||
height={400}
|
||||
style={{ width: "100%", height: "auto" }}
|
||||
/>
|
||||
{value.caption && (
|
||||
<p className={styles.imageCaption}>{value.caption}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
marks: {
|
||||
link: ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
value?: LinkValue
|
||||
}) => (
|
||||
<a
|
||||
href={value?.href ?? "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const BlogPost = async ({ params }: BlogPageParams) => {
|
||||
const { slug }: { slug: string } = await params
|
||||
const locale = await getLocale()
|
||||
|
||||
const data = await getBlogPost(slug)
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Blog post not found")
|
||||
}
|
||||
|
||||
const createdAt = new Date(data._createdAt).toLocaleDateString(locale, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.backButton}>
|
||||
<Back />
|
||||
</div>
|
||||
<article className={styles.article}>
|
||||
<header className={styles.header}>
|
||||
{data.mainImage && (
|
||||
<Image
|
||||
src={urlFor(data.mainImage).url()}
|
||||
alt={data.title}
|
||||
width={800}
|
||||
height={400}
|
||||
className={styles.headerImage}
|
||||
/>
|
||||
)}
|
||||
<h1 className={styles.title}>{data.title}</h1>
|
||||
<p className={styles.excerpt}>{data.excerpt}</p>
|
||||
<div className={styles.author}>
|
||||
{data.author?.image && (
|
||||
<Image
|
||||
className="circular"
|
||||
alt={`Forfatteren av innlegget, ${data.author.name}`}
|
||||
src={urlFor(data.author.image).width(50).height(50).url()}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.authorNameSection}>
|
||||
<p>{data.author?.name}</p>
|
||||
<p>{createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className={styles.content}>
|
||||
<PortableText value={data.body} components={components} />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<Footer />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
@@ -1,12 +1,7 @@
|
||||
body {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
min-height: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
@@ -29,8 +24,13 @@ body {
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
font-weight: 100;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
.metadata > *:hover {
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 1rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
import Back from "@/components/ui/back"
|
||||
|
||||
import { getEvent } from "@/lib/sanity"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { getLocale } from "next-intl/server"
|
||||
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
|
||||
|
||||
import * as styles from "./page.module.css"
|
||||
|
||||
interface EventPageParams {
|
||||
params: Promise<{
|
||||
slug: string
|
||||
}>
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: EventPageParams): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const data = await getEvent(slug)
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: "Event Not Found | UiO Gaming",
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
return date.toLocaleString("no", options)
|
||||
}
|
||||
|
||||
return generateSeoMetadata({
|
||||
title: data.title,
|
||||
description: `Tid: ${formatDate(data.date)}\nSted: ${data.location}\n\n${data.description}`,
|
||||
})
|
||||
}
|
||||
|
||||
export function generateViewport(): Viewport {
|
||||
return generateSeoViewport()
|
||||
}
|
||||
|
||||
const Event = async ({ params }: EventPageParams) => {
|
||||
const { slug }: { slug: string } = await params
|
||||
const locale = await getLocale()
|
||||
|
||||
const data = await getEvent(slug)
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Event not found")
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Back />
|
||||
<div className={styles.event}>
|
||||
<h1>{data.title}</h1>
|
||||
<div className={styles.metadata}>
|
||||
<FaMapMarkerAlt />
|
||||
<p>{data.location}</p>
|
||||
</div>
|
||||
<div className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>{formatDate(data.date)}</p>
|
||||
</div>
|
||||
<p className={styles.description}>{data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Event
|
||||
@@ -3,6 +3,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import Layout from "@/app/layout"
|
||||
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
import { useTranslations } from "next-intl"
|
||||
import Link from "next/link"
|
||||
|
||||
import * as styles from "./global-not-found.module.css"
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return generateSeoMetadata({
|
||||
title: "404: Ikke funnet",
|
||||
description: "Uh oh :(",
|
||||
})
|
||||
}
|
||||
|
||||
export function generateViewport(): Viewport {
|
||||
return generateSeoViewport()
|
||||
}
|
||||
|
||||
const NotFoundPage = () => {
|
||||
const t = useTranslations("404")
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.centeredText}>{t("title")}</h1>
|
||||
<p className={styles.centeredText}>
|
||||
{t("text")}{" "}
|
||||
<span role="img" aria-label="eyes">
|
||||
👀
|
||||
</span>
|
||||
</p>
|
||||
<Link href="/" className={`${styles.centeredText} ${styles.homeLink}`}>
|
||||
{t("back")}
|
||||
</Link>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
||||
@@ -1,18 +1,20 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,400;0,900;1,100;1,400;1,900&display=swap");
|
||||
|
||||
/* ===== Variables ===== */
|
||||
|
||||
:root {
|
||||
--main-color: rgb(226, 35, 26);
|
||||
--main-background-color: rgb(22, 20, 31);
|
||||
--secondary-background-color: rgb(44, 53, 69);
|
||||
--main-color: #7d36e7;
|
||||
--main-background-color: #1d1320;
|
||||
--secondary-background-color: #261f31;
|
||||
--footer-color: rgb(12, 12, 15);
|
||||
|
||||
--header-color: rgb(255, 255, 255);
|
||||
--text-color: rgb(228, 228, 228);
|
||||
--light-text-color: rgb(190, 190, 190);
|
||||
--link-color: rgb(226, 35, 26);
|
||||
--link-color-hover: rgb(255, 157, 160);
|
||||
--text-color: #eeeeee;
|
||||
--light-text-color: #cccccc;
|
||||
--link-color: #7d36e7;
|
||||
--link-color-hover: #d1b3ff;
|
||||
|
||||
--main-font: "Roboto", sans-serif;
|
||||
--main-font: "Nunito", sans-serif;
|
||||
|
||||
--card-section-divider: 2px solid rgba(128, 128, 128, 0.5);
|
||||
--blog-header-divider: 1px solid rgba(128, 128, 128, 0.25);
|
||||
@@ -90,8 +92,6 @@ select {
|
||||
|
||||
/* ===== Base styling ===== */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,400;0,900;1,100;1,400;1,900&display=swap");
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
}
|
||||
@@ -107,7 +107,7 @@ select {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
a::selection {
|
||||
color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -161,6 +161,7 @@ section {
|
||||
padding: 7em 1em;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
section:nth-child(2n + 1) {
|
||||
background-color: var(--secondary-background-color);
|
||||
@@ -173,9 +174,9 @@ section h2 {
|
||||
text-align: center;
|
||||
justify-self: center;
|
||||
}
|
||||
@media (min-width: 60em) {
|
||||
@media (min-width: 70em) {
|
||||
section {
|
||||
padding: 10em 5em;
|
||||
padding: 10em 4em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +220,13 @@ section h2 {
|
||||
mask-image: radial-gradient(white, black);
|
||||
-webkit-mask-image: -webkit-radial-gradient(white, black);
|
||||
}
|
||||
@media (min-width: 60em) {
|
||||
.photo-credit,
|
||||
.photo-credit:hover {
|
||||
color: var(--light-text-color);
|
||||
text-align: end;
|
||||
place-self: end;
|
||||
}
|
||||
@media (min-width: 70em) {
|
||||
.no-mobile {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import "@/app/globals.css"
|
||||
|
||||
import { LocaleProvider } from "@/components/localeProvider"
|
||||
import UmamiAnalytics from "@/components/umamiAnalytics"
|
||||
|
||||
import { getAnalyticsConfig } from "@/lib/analyticsConfig"
|
||||
|
||||
import type { Metadata } from "next"
|
||||
import { getLocale } from "next-intl/server"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "UiO Gaming",
|
||||
description:
|
||||
"En hobbyforening som ønsker skape et sosialt miljø rundt gaming og e-sport på UiO!",
|
||||
icons: {
|
||||
icon: "/logo_2024_neon_pink.png",
|
||||
shortcut: "/logo_2024_neon_pink.png",
|
||||
apple: "/logo_2024_neon_pink.png",
|
||||
},
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const locale = (await getLocale()) as "no" | "en"
|
||||
const messages = (await import(`../../messages/${locale}.json`)).default // TODO: duplicate of request.ts?
|
||||
|
||||
const analyticsConfig = getAnalyticsConfig()
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<body>
|
||||
<LocaleProvider locale={locale} messages={messages}>
|
||||
<main>{children}</main>
|
||||
{analyticsConfig?.enabled && (
|
||||
<UmamiAnalytics
|
||||
websiteId={analyticsConfig.websiteId}
|
||||
src={analyticsConfig.src}
|
||||
domains={analyticsConfig.domains}
|
||||
/>
|
||||
)}
|
||||
</LocaleProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Layout from "@/app/layout"
|
||||
|
||||
import About from "@/components/page-sections/about"
|
||||
import BoardMembers from "@/components/page-sections/boardMembers"
|
||||
import Contact from "@/components/page-sections/contact"
|
||||
import Events from "@/components/page-sections/events"
|
||||
import Footer from "@/components/page-sections/footer"
|
||||
import Hero from "@/components/page-sections/hero"
|
||||
import Office from "@/components/page-sections/office"
|
||||
import { generateSeoMetadata, generateSeoViewport } from "@/components/seo"
|
||||
|
||||
import { Metadata, Viewport } from "next"
|
||||
|
||||
export const revalidate = 3600 // 1 hour
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return generateSeoMetadata({})
|
||||
}
|
||||
|
||||
export function generateViewport(): Viewport {
|
||||
return generateSeoViewport()
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
<Hero />
|
||||
<About />
|
||||
<Events />
|
||||
<Office />
|
||||
<BoardMembers />
|
||||
<Contact />
|
||||
<Footer />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./back.module.css"
|
||||
|
||||
import { FaArrowLeft } from "react-icons/fa"
|
||||
|
||||
const Back = () => (
|
||||
<button
|
||||
className={`${styles.container} ${styles.button}`}
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<FaArrowLeft />
|
||||
<p>Tilbake</p>
|
||||
</button>
|
||||
)
|
||||
|
||||
export default Back
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./memberButton.module.css"
|
||||
|
||||
const MemberButton = () => (
|
||||
<a
|
||||
href="https://innmelding.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.button}
|
||||
>
|
||||
Bli medlem!
|
||||
</a>
|
||||
)
|
||||
export default MemberButton
|
||||
@@ -1,16 +0,0 @@
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 1.6rem;
|
||||
background-color: var(--main-color);
|
||||
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 999em;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
||||
.button:hover {
|
||||
color: white;
|
||||
background-color: rgb(255, 62, 52);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<main>{children}</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -0,0 +1,24 @@
|
||||
.button {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
|
||||
position: fixed;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 0;
|
||||
|
||||
background: pink;
|
||||
color: black;
|
||||
border: 1px solid black;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 0.5rem;
|
||||
padding-inline: 0.5em;
|
||||
}
|
||||
.button:hover {
|
||||
background: #ff7b7b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import { GB, NO } from "country-flag-icons/react/3x2"
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
import { ReactNode, useEffect, useState } from "react"
|
||||
|
||||
import * as styles from "./localeProvider.module.css"
|
||||
|
||||
type LocaleProviderProps = {
|
||||
locale: "no" | "en"
|
||||
messages: Record<string, string>
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function LocaleProvider({
|
||||
locale: serverLocale,
|
||||
messages,
|
||||
children,
|
||||
}: LocaleProviderProps) {
|
||||
const [locale, setLocale] = useState<"no" | "en">(serverLocale)
|
||||
const [loadedMessages, setLoadedMessages] =
|
||||
useState<Record<string, string>>(messages)
|
||||
|
||||
useEffect(() => {
|
||||
const match = document.cookie.match(/(?:^|; )locale=([^;]+)/)
|
||||
const saved = match ? decodeURIComponent(match[1]) : null
|
||||
|
||||
if (saved && saved !== locale) {
|
||||
setLocale(saved as "no" | "en")
|
||||
import(`../../messages/${saved}.json`).then(module => {
|
||||
setLoadedMessages(module.default)
|
||||
})
|
||||
}
|
||||
}, [locale])
|
||||
|
||||
const changeLocale = (newLocale: "no" | "en") => {
|
||||
setLocale(newLocale)
|
||||
document.cookie = `locale=${encodeURIComponent(
|
||||
newLocale
|
||||
)}; path=/; max-age=31536000; SameSite=Lax`
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider locale={locale} messages={loadedMessages}>
|
||||
{children}
|
||||
|
||||
<div className={styles.languageSwitcher}>
|
||||
{locale === "no" ? (
|
||||
<button onClick={() => changeLocale("en")} className={styles.button}>
|
||||
<GB className={styles.flag} title="English flag" />
|
||||
English
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => changeLocale("no")} className={styles.button}>
|
||||
<NO className={styles.flag} title="Norwegian flag" />
|
||||
Norsk
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</NextIntlClientProvider>
|
||||
)
|
||||
}
|
||||
@@ -4,16 +4,11 @@
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.joinSection {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
place-items: center;
|
||||
margin: 3.5rem 0;
|
||||
margin-top: 3.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.joinSection h3 {
|
||||
@@ -24,8 +19,19 @@
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.image {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
.image img {
|
||||
border-radius: 1rem;
|
||||
object-fit: cover;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
@media (min-width: 60em) {
|
||||
@media (min-width: 70em) {
|
||||
.container {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
@@ -36,4 +42,10 @@
|
||||
.buttons {
|
||||
flex-direction: row;
|
||||
}
|
||||
.image {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
justify-self: right;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import DiscordButton from "@components/ui/discordButton"
|
||||
import MemberButton from "@components/ui/memberButton"
|
||||
import OtherButton from "@components/ui/otherButton"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import Image from "next/image"
|
||||
|
||||
import * as styles from "./about.module.css"
|
||||
|
||||
const About = () => {
|
||||
const t = useTranslations("about")
|
||||
const tCommon = useTranslations("common")
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className={styles.container}>
|
||||
<div className={"no-mobile " + styles.image}>
|
||||
<Image
|
||||
src="/about.jpg"
|
||||
alt={t("imageAlt")}
|
||||
width={640}
|
||||
height={480}
|
||||
/>
|
||||
<p className="photo-credit">
|
||||
{tCommon("photo")}: 장태민, Studentenes Fotoklubb
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="left-section-header">{t("title")}</h2>
|
||||
<div>
|
||||
<p>{t("text")}</p>
|
||||
<div className={styles.joinSection}>
|
||||
<h3>{t("interested")}</h3>
|
||||
<div className={styles.buttons}>
|
||||
<MemberButton />
|
||||
<DiscordButton />
|
||||
<OtherButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
@@ -1,9 +1,9 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
justify-content: space-evenly;
|
||||
gap: 7rem;
|
||||
gap: 4rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
@media (min-width: 500px) {
|
||||
.card {
|
||||
width: 35ch;
|
||||
width: 37ch;
|
||||
}
|
||||
}
|
||||
.card > p {
|
||||
@@ -39,11 +39,12 @@
|
||||
}
|
||||
.headerImageContainer {
|
||||
border: 5px solid white;
|
||||
display: inline-block;
|
||||
}
|
||||
/* .headerImage {
|
||||
box-shadow: inset 0 0 10px #000000;
|
||||
} */
|
||||
.headerImageContainer img {
|
||||
object-fit: cover;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-bottom: var(--card-section-divider);
|
||||
@@ -53,8 +54,9 @@
|
||||
}
|
||||
.info > p {
|
||||
font-weight: 100;
|
||||
font-size: 1.25rem;
|
||||
font-size: 1.2rem;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
.info > h3 {
|
||||
margin: 0.5rem 0;
|
||||
@@ -64,6 +66,7 @@
|
||||
border-top: var(--card-section-divider);
|
||||
width: 100%;
|
||||
font-size: 1.1rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
.footer > p {
|
||||
margin-top: 2rem;
|
||||
@@ -71,7 +74,7 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (min-width: 60em) {
|
||||
@media (min-width: 70em) {
|
||||
.header {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { urlFor } from "@/lib/imageUrlBuilder"
|
||||
import { sanityClient } from "@/lib/sanity"
|
||||
|
||||
import { getLocale, getTranslations } from "next-intl/server"
|
||||
import Image from "next/image"
|
||||
|
||||
import * as styles from "./boardMembers.module.css"
|
||||
|
||||
interface BoardMember {
|
||||
_id: string
|
||||
boardMember: boolean
|
||||
contactInfo?: string
|
||||
name: string
|
||||
role?: {
|
||||
no: string
|
||||
en: string
|
||||
}
|
||||
image?: {
|
||||
asset?: {
|
||||
_id: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
bio?: {
|
||||
no: string
|
||||
en: string
|
||||
}
|
||||
}
|
||||
|
||||
const BoardMembers = async () => {
|
||||
const t = await getTranslations("boardMembers")
|
||||
const locale = (await getLocale()) as "no" | "en"
|
||||
|
||||
const query = `
|
||||
*[_type == "author" && boardMember == true] | order(orderRank asc) {
|
||||
_id,
|
||||
boardMember,
|
||||
contactInfo,
|
||||
name,
|
||||
role {
|
||||
no,
|
||||
en
|
||||
},
|
||||
image {
|
||||
asset->{
|
||||
_id,
|
||||
url
|
||||
}
|
||||
},
|
||||
bio {
|
||||
no,
|
||||
en
|
||||
}
|
||||
}
|
||||
`
|
||||
const data: BoardMember[] = await sanityClient.fetch(query)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>{t("title")}</h2>
|
||||
<div className={styles.container}>
|
||||
{data.map((member: BoardMember) => (
|
||||
<article key={member._id} className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={`circular-container ${styles.headerImageContainer}`}
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
member.image?.asset?.url
|
||||
? urlFor(member.image.asset.url)
|
||||
.width(512)
|
||||
.height(512)
|
||||
.url()
|
||||
: "/person.png"
|
||||
}
|
||||
width={256}
|
||||
height={256}
|
||||
className="circular"
|
||||
alt={`${t("imageAlt")}, ${member.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h3>{member.name}</h3>
|
||||
<p>{member.role?.[locale]}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{member.bio?.[locale]}</p>
|
||||
<div className={styles.footer}>
|
||||
<p className="text-center">
|
||||
{member.contactInfo ? (
|
||||
member.contactInfo
|
||||
) : (
|
||||
<i>{t("noContact")}</i>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default BoardMembers
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import * as styles from "./contact.module.css"
|
||||
|
||||
import {
|
||||
FaAt,
|
||||
FaDiscord,
|
||||
FaFacebook,
|
||||
FaGithub,
|
||||
FaInstagram,
|
||||
FaTiktok,
|
||||
FaTwitch,
|
||||
FaYoutube,
|
||||
} from "react-icons/fa"
|
||||
|
||||
const Contact = () => {
|
||||
const t = useTranslations("contact")
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="section-center-header">{t("title")}</h2>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.item}>
|
||||
<FaAt />
|
||||
<a href="mailto:styret@uiogaming.no" target="_blank" rel="noreferrer">
|
||||
{t("email")}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaDiscord />
|
||||
<a
|
||||
href="https://discord.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaInstagram />
|
||||
<a
|
||||
href="https://instagram.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Instagram
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaTiktok />
|
||||
<a
|
||||
href="https://tiktok.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
TikTok
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaTwitch />
|
||||
<a
|
||||
href="https://twitch.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Twitch
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaYoutube />
|
||||
<a
|
||||
href="https://youtube.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
YouTube
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaFacebook />
|
||||
<a
|
||||
href="https://facebook.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Facebook
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaGithub />
|
||||
<a
|
||||
href="https://github.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Contact
|
||||
@@ -5,7 +5,7 @@
|
||||
.container {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 6em;
|
||||
gap: 4em;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
.container > h2 {
|
||||
@@ -23,7 +23,7 @@
|
||||
gap: 2rem;
|
||||
}
|
||||
.events > *,
|
||||
*:hover {
|
||||
.events > *:hover {
|
||||
color: white;
|
||||
}
|
||||
.events > h2 {
|
||||
@@ -48,18 +48,30 @@
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.card > div {
|
||||
font-weight: 100;
|
||||
font-weight: 200;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.card > div * {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.metadata {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
font-weight: 100;
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
.metadata > *:hover {
|
||||
color: var(--light-text-color);
|
||||
}
|
||||
|
||||
.image {
|
||||
grid-row: 1 / -1;
|
||||
align-self: center;
|
||||
justify-self: left;
|
||||
margin-top: 2em;
|
||||
}
|
||||
@media (min-width: 60em) {
|
||||
@media (min-width: 70em) {
|
||||
.stretch {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -67,15 +79,16 @@
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
}
|
||||
.container > h2 {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.sectionHeader {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
.image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
height: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.readMore {
|
||||
@@ -0,0 +1,113 @@
|
||||
import { getAllEvents } from "@/lib/sanity"
|
||||
|
||||
import { getLocale, getTranslations } from "next-intl/server"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
|
||||
|
||||
import * as styles from "./events.module.css"
|
||||
|
||||
interface Event {
|
||||
_id: string
|
||||
title: string
|
||||
location: string
|
||||
date: string
|
||||
slug: {
|
||||
current: string
|
||||
}
|
||||
description: string
|
||||
}
|
||||
|
||||
const Events = async () => {
|
||||
const t = await getTranslations("events")
|
||||
const tCommon = await getTranslations("common")
|
||||
const locale = await getLocale()
|
||||
|
||||
const data: Event[] = await getAllEvents()
|
||||
|
||||
const currentDate = new Date()
|
||||
const filteredEvents = data
|
||||
.filter((event: Event) => {
|
||||
return currentDate <= new Date(event.date)
|
||||
})
|
||||
.slice(0, 2)
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
timeZone: "Europe/Oslo",
|
||||
}
|
||||
|
||||
return date.toLocaleString(locale, options)
|
||||
}
|
||||
|
||||
const mapEvents = (events: Event[]) => {
|
||||
if (events.length !== 0) {
|
||||
return events.map((event: Event) => (
|
||||
<Link key={event._id} href={"/event/" + event.slug.current}>
|
||||
<article className={styles.card}>
|
||||
<h3>{event.title}</h3>
|
||||
<div>
|
||||
<div className={styles.metadata}>
|
||||
<FaMapMarkerAlt />
|
||||
<p>{event.location}</p>
|
||||
</div>
|
||||
<div className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>{formatDate(event.date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
{event.description.length < 70 ? (
|
||||
event.description
|
||||
) : (
|
||||
<>
|
||||
{event.description.substring(0, 70)}...{" "}
|
||||
<span className={styles.readMore}>{t("readMore")}</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</article>
|
||||
</Link>
|
||||
))
|
||||
} else {
|
||||
return (
|
||||
<h3 className={styles.noEvents}>
|
||||
{t("noEvents")}
|
||||
<br />
|
||||
<br />
|
||||
(っ˘̩╭╮˘̩)っ
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.sectionHeader}>{t("title")}</h2>
|
||||
<div className={styles.events}>{mapEvents(filteredEvents)}</div>
|
||||
<div className={"no-mobile " + styles.image}>
|
||||
<Image
|
||||
src="/event.jpg"
|
||||
width={850}
|
||||
height={480}
|
||||
style={{ borderRadius: "1rem", objectFit: "cover", height: "100%" }}
|
||||
alt={t("imageAlt")}
|
||||
/>
|
||||
<p className="photo-credit">
|
||||
{tCommon("photo")}: 장태민, Studentenes Fotoklubb
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Events
|
||||
@@ -1,26 +1,37 @@
|
||||
footer {
|
||||
.container {
|
||||
background-color: var(--footer-color);
|
||||
padding: 10rem 3rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
justify-items: space-around;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
padding: 10rem 3rem;
|
||||
gap: 4rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 2rem;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.orgInfo {
|
||||
text-align: center;
|
||||
justify-self: center;
|
||||
}
|
||||
.orgInfo * {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.links {
|
||||
justify-self: center;
|
||||
}
|
||||
.links * {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
.logo {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 70em) {
|
||||
.footer {
|
||||
@@ -28,6 +39,10 @@ footer {
|
||||
}
|
||||
.orgInfo {
|
||||
text-align: left;
|
||||
justify-self: center;
|
||||
}
|
||||
.logo {
|
||||
justify-self: right;
|
||||
}
|
||||
.links {
|
||||
justify-self: start;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import Image from "next/image"
|
||||
|
||||
import * as styles from "./footer.module.css"
|
||||
|
||||
const Footer = () => {
|
||||
const t = useTranslations("footer")
|
||||
const tCommon = useTranslations("common")
|
||||
const locale = useLocale()
|
||||
|
||||
const statutesUrl =
|
||||
locale === "en"
|
||||
? "https://statutes.uiogaming.no"
|
||||
: "https://vedtekter.uiogaming.no"
|
||||
|
||||
return (
|
||||
<footer className={styles.container}>
|
||||
<div className={styles.footer}>
|
||||
<Image
|
||||
src="/logo_2024_neon_trans.png"
|
||||
alt="UiO Gaming logo"
|
||||
className={styles.logo}
|
||||
width={187}
|
||||
height={256}
|
||||
/>
|
||||
<div className={styles.links}>
|
||||
<ul className="remove-bullets">
|
||||
<li>
|
||||
<a href={statutesUrl} rel="noreferrer">
|
||||
{tCommon("statutes")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://innmelding.uiogaming.no" rel="noreferrer">
|
||||
{tCommon("join")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://klage.uiogaming.no" rel="noreferrer">
|
||||
{tCommon("complaint")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/UiO-Gaming/uiogaming.no/"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t("sourceCode")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://admin.uiogaming.no" rel="noreferrer">
|
||||
{t("admin")}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.orgInfo}>
|
||||
<p>{t("orgNum")}: 925 719 153</p>
|
||||
<address>Slemdalsveien 15, 0369 Oslo</address>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
background-color: #7c5ca8;
|
||||
background-image: url("/mustafa.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 90%, 0% 100%);
|
||||
|
||||
padding: 1rem;
|
||||
padding-bottom: 10vh;
|
||||
min-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
max-width: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 2em;
|
||||
margin-block: 2.5em;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.content > p {
|
||||
max-width: 40ch;
|
||||
color: #130729;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.content > h1 {
|
||||
color: #130729;
|
||||
font-size: 4rem;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useTranslations } from "next-intl"
|
||||
import Image from "next/image"
|
||||
|
||||
import * as styles from "./hero.module.css"
|
||||
|
||||
const Hero = () => {
|
||||
const t = useTranslations("hero")
|
||||
|
||||
return (
|
||||
<header id="hjem" className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<Image
|
||||
src="/logo_2024_black_trans.png"
|
||||
alt="UiO Gaming logo"
|
||||
width={187}
|
||||
height={256}
|
||||
/>
|
||||
<p>{t("text")}</p>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Hero
|
||||
@@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.panoramaContainer {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #000000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panoramaContainer > div {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
|
||||
|
||||
import * as styles from "./office.module.css"
|
||||
|
||||
const Office = () => {
|
||||
const t = useTranslations("office")
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>{t("title")}</h2>
|
||||
<div className={styles.container}>
|
||||
<p>{t("text")}</p>
|
||||
<p>{t("tip")}</p>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "500px",
|
||||
background: "#000000",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<ReactPhotoSphereViewer
|
||||
src="/3D_office_2024_11_03.jpg"
|
||||
height="100%"
|
||||
width="100%"
|
||||
defaultZoomLvl={0}
|
||||
navbar={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Office
|
||||
@@ -1,61 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./about.module.css"
|
||||
|
||||
import { graphql, useStaticQuery } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
import DiscordButton from "../items/discordButton"
|
||||
import MemberButton from "../items/memberButton"
|
||||
|
||||
const About = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
logo: file(relativePath: { eq: "about.jpg" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(
|
||||
placeholder: BLURRED
|
||||
transformOptions: { fit: CONTAIN }
|
||||
width: 650
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className={styles.container}>
|
||||
<div className="no-mobile">
|
||||
<GatsbyImage
|
||||
image={data.logo.childImageSharp.gatsbyImageData}
|
||||
alt="Medlemmer som spiller brettspill"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="left-section-header">Hvem er vi?</h2>
|
||||
<div>
|
||||
<p>
|
||||
UiO Gaming er en forening med hovedfokus på å danne et
|
||||
inkluderende fellesskap rundt videospill og e-sport. Hos oss er
|
||||
det enkelt å skaffe seg venner, ettersom alle har felles
|
||||
interesser. Vi arrangerer ukentlige spillkvelder og fysiske
|
||||
sosiale arrangementer, som for eksempel brettspillkvelder og
|
||||
sosiale sammenkomster. Vår hovedkanal er en aktiv Discord-server
|
||||
hvor man kan møte nye folk med felles interesser og game med andre
|
||||
studenter.
|
||||
</p>
|
||||
<div className={styles.joinSection}>
|
||||
<h3>Interessert?</h3>
|
||||
<div className={styles.buttons}>
|
||||
<MemberButton />
|
||||
<DiscordButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default About
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./boardMembers.module.css"
|
||||
|
||||
import { graphql, useStaticQuery } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
const BoardMembers = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
allSanityAuthor(
|
||||
filter: { boardMember: { eq: true } }
|
||||
sort: { fields: order }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
_id
|
||||
boardMember
|
||||
email
|
||||
name
|
||||
role
|
||||
image {
|
||||
asset {
|
||||
gatsbyImageData(width: 256, height: 256)
|
||||
}
|
||||
}
|
||||
bio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Styret</h2>
|
||||
<div className={styles.container}>
|
||||
{data.allSanityAuthor.edges.map(({ node: member }) => (
|
||||
<article key={member._id} className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={`circular-container ${styles.headerImageContainer}`}
|
||||
>
|
||||
<GatsbyImage
|
||||
image={member.image.asset.gatsbyImageData}
|
||||
className="circular"
|
||||
alt={"Bilde av styemedlem, " + member.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h3>{member.name}</h3>
|
||||
<p>{member.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{member.bio}</p>
|
||||
<div className={styles.footer}>
|
||||
<p className="text-center">{member.email}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default BoardMembers
|
||||
@@ -1,76 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./contact.module.css"
|
||||
|
||||
import {
|
||||
FaYoutube,
|
||||
FaTwitch,
|
||||
FaFacebook,
|
||||
FaGithub,
|
||||
FaDiscord,
|
||||
FaAt,
|
||||
FaInstagram,
|
||||
} from "react-icons/fa"
|
||||
|
||||
const Contact = () => (
|
||||
<section>
|
||||
<h2 className="section-center-header">Finn oss</h2>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.item}>
|
||||
<FaAt />
|
||||
<a
|
||||
href="mailto:styret@uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
E-post
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaDiscord />
|
||||
<a href="https://discord.uiogaming.no" target="_blank" rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaFacebook />
|
||||
<a
|
||||
href="https://facebook.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Facebook
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaInstagram />
|
||||
<a
|
||||
href="https://instagram.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Instagram
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaYoutube />
|
||||
<a href="https://youtube.uiogaming.no" target="_blank" rel="noreferrer">
|
||||
YouTube
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaTwitch />
|
||||
<a href="https://twitch.uiogaming.no" target="_blank" rel="noreferrer">
|
||||
Twitch
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<FaGithub />
|
||||
<a href="https://github.uiogaming.no" target="_blank" rel="noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
export default Contact
|
||||
@@ -1,101 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./events.module.css"
|
||||
|
||||
import { Link, graphql, useStaticQuery } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
import moment from "moment-timezone"
|
||||
import "moment/locale/nb"
|
||||
|
||||
import { FaMapMarkerAlt, FaCalendarAlt } from "react-icons/fa"
|
||||
|
||||
const Events = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
placeholder: file(relativePath: { eq: "event.jpg" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(
|
||||
placeholder: BLURRED
|
||||
transformOptions: { fit: COVER }
|
||||
width: 650
|
||||
)
|
||||
}
|
||||
}
|
||||
allSanityEvent(sort: { fields: date, order: ASC }) {
|
||||
edges {
|
||||
node {
|
||||
_id
|
||||
title
|
||||
location
|
||||
date
|
||||
slug {
|
||||
current
|
||||
}
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
moment.locale("nb_NO")
|
||||
const currentDate = moment().utc()
|
||||
|
||||
const filteredEvents = data.allSanityEvent.edges
|
||||
.filter(({ node: event }) => {
|
||||
return currentDate <= moment.utc(event.date)
|
||||
})
|
||||
.slice(0, 2)
|
||||
|
||||
const mapEvents = events => {
|
||||
if (events.length !== 0) {
|
||||
return events.map(({ node: event }) => (
|
||||
<Link key={event._id} to={"event/" + event.slug.current}>
|
||||
<article className={styles.card}>
|
||||
<h3>{event.title}</h3>
|
||||
<div>
|
||||
<div>
|
||||
<FaMapMarkerAlt />
|
||||
<p>{event.location}</p>
|
||||
</div>
|
||||
<div>
|
||||
<FaCalendarAlt />
|
||||
<p>
|
||||
{moment(event.date)
|
||||
.tz("Europe/Oslo")
|
||||
.format("dddd Do MMMM, [kl.] H:mm")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>{event.description.slice(0, 120)}... <span className={styles.readMore}>Les mer</span></p>
|
||||
</article>
|
||||
</Link>
|
||||
))
|
||||
} else {
|
||||
return (
|
||||
<h3 className={styles.noEvents}>
|
||||
Ingen kommende arrangementer
|
||||
<br />
|
||||
<br />
|
||||
(っ˘̩╭╮˘̩)っ
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.stretch}>
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.sectionHeader}>Hva skjer?</h2>
|
||||
<div className={styles.events}>{mapEvents(filteredEvents)}</div>
|
||||
<div className={"no-mobile " + styles.image}>
|
||||
<GatsbyImage
|
||||
image={data.placeholder.childImageSharp.gatsbyImageData}
|
||||
alt="Medlem som spiller VR"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Events
|
||||
@@ -1,68 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./footer.module.css"
|
||||
|
||||
import { graphql, useStaticQuery } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
const Footer = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
logo: file(relativePath: { eq: "logo.jpg" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(
|
||||
placeholder: BLURRED
|
||||
transformOptions: { fit: COVER }
|
||||
width: 256
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<footer>
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
<GatsbyImage
|
||||
image={data.logo.childImageSharp.gatsbyImageData}
|
||||
alt="UiO Gaming logo"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
<ul className="remove-bullets">
|
||||
<li>
|
||||
<a href="https://vedtekter.uiogaming.no" rel="noreferrer">
|
||||
Vedtekter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://innmelding.uiogaming.no" rel="noreferrer">
|
||||
Bli medlem
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/UiO-Gaming/uiogaming-website/"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Kildekode
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://admin.uiogaming.no" rel="noreferrer">
|
||||
Adminområdet
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={styles.orgInfo}>
|
||||
<p>Orgnr.: 925 719 153</p>
|
||||
<address>Slemdalsveien 15, 0369 Oslo</address>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./header.module.css"
|
||||
|
||||
import { graphql, useStaticQuery } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
const Header = () => {
|
||||
const data = useStaticQuery(graphql`
|
||||
query {
|
||||
placeholder: file(relativePath: { eq: "logo.jpg" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(
|
||||
placeholder: BLURRED
|
||||
transformOptions: { fit: CONTAIN }
|
||||
width: 256
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
return (
|
||||
<header id="hjem" className={styles.container}>
|
||||
<GatsbyImage
|
||||
image={data.placeholder.childImageSharp.gatsbyImageData}
|
||||
alt="UiO Gaming logo"
|
||||
/>
|
||||
|
||||
<h1>UiO Gaming</h1>
|
||||
<p>
|
||||
En hobbyforening som ønsker å skape et sosialt miljø rundt gaming og
|
||||
e-sport på UiO
|
||||
</p>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,23 +0,0 @@
|
||||
.container {
|
||||
background-image: url("../../images/keyboard.jpg"); /* Placeholder image */
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% 90%, 0% 100%);
|
||||
|
||||
padding: 1rem;
|
||||
padding-bottom: 10vh;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
|
||||
max-width: none;
|
||||
text-align: center;
|
||||
}
|
||||
.container > p {
|
||||
max-width: 40ch;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./office.module.css"
|
||||
|
||||
import ReactPannellum from "react-pannellum"
|
||||
|
||||
const config = {
|
||||
autoLoad: true,
|
||||
}
|
||||
|
||||
const Office = () => (
|
||||
<section>
|
||||
<h2>Kontoret</h2>
|
||||
<div className={styles.container}>
|
||||
<p>
|
||||
Vi har et foreningskontor på Chateau Neuf der man kan bruke utstyret
|
||||
vårt og henge med andre medlemmer!
|
||||
</p>
|
||||
|
||||
<ReactPannellum
|
||||
id="1"
|
||||
sceneId="office"
|
||||
config={config}
|
||||
imageSource="/3D_office_2023_09_15.jpg"
|
||||
style={{ width: "100%", aspectRatio: "1 / 1", background: "#000000" }} // yes, apparantly applying a css class to the component doesn't work
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
export default Office
|
||||
@@ -1,11 +0,0 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import PropTypes from "prop-types"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { useStaticQuery, graphql } from "gatsby"
|
||||
|
||||
function Seo({ description, lang, meta, imageURL, title, author }) {
|
||||
const { site } = useStaticQuery(
|
||||
graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
author
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
const metaAuthor = author || site.siteMetadata.author
|
||||
const metaDescription = description || site.siteMetadata.description
|
||||
const defaultTitle = site.siteMetadata?.title
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang,
|
||||
}}
|
||||
title={title ? title : defaultTitle}
|
||||
titleTemplate={title ? `%s | ${defaultTitle}` : defaultTitle}
|
||||
meta={[
|
||||
{
|
||||
name: `author`,
|
||||
content: metaAuthor,
|
||||
},
|
||||
{
|
||||
name: `description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
{
|
||||
property: `og:site_name`,
|
||||
content: site.siteMetadata.title,
|
||||
},
|
||||
{
|
||||
property: `og:title`,
|
||||
content: title ? title : defaultTitle,
|
||||
},
|
||||
{
|
||||
property: `og:description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
{
|
||||
property: `og:type`,
|
||||
content: `website`,
|
||||
},
|
||||
{
|
||||
property: `og:locale`,
|
||||
content: `nb_NO`,
|
||||
},
|
||||
{
|
||||
name: `twitter:creator`,
|
||||
content: metaAuthor,
|
||||
},
|
||||
{
|
||||
name: `twitter:title`,
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
name: `twitter:description`,
|
||||
content: metaDescription,
|
||||
},
|
||||
]
|
||||
.concat(
|
||||
imageURL
|
||||
? [
|
||||
{
|
||||
property: "og:image",
|
||||
content: imageURL,
|
||||
},
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary_large_image",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
property: "og:image",
|
||||
content: `${site.siteMetadata.url}/logo.jpg`,
|
||||
},
|
||||
{
|
||||
name: "twitter:card",
|
||||
content: "summary",
|
||||
},
|
||||
]
|
||||
)
|
||||
.concat(meta)}
|
||||
>
|
||||
<script
|
||||
async
|
||||
defer
|
||||
data-website-id="c375ccba-0b3e-4bf0-a4c9-165bd9b0176c"
|
||||
src="https://umami.uiogaming.no/umami.js"
|
||||
></script>
|
||||
</Helmet>
|
||||
)
|
||||
}
|
||||
|
||||
Seo.defaultProps = {
|
||||
lang: `no`,
|
||||
meta: [],
|
||||
description: ``,
|
||||
}
|
||||
|
||||
Seo.propTypes = {
|
||||
description: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
meta: PropTypes.arrayOf(PropTypes.object),
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
export default Seo
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Metadata, Viewport } from "next"
|
||||
|
||||
type SeoProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
imageURL?: string
|
||||
author?: string
|
||||
}
|
||||
|
||||
export function generateSeoMetadata({
|
||||
title,
|
||||
description,
|
||||
imageURL,
|
||||
author,
|
||||
}: SeoProps): Metadata {
|
||||
const defaultTitle = "UiO Gaming"
|
||||
const metaDescription =
|
||||
description ||
|
||||
"En hobbyforening som ønsker skape et sosialt miljö rundt gaming og e-sport på UiO!"
|
||||
const siteUrl = "https://uiogaming.no"
|
||||
const themeColor = "#7d36e7"
|
||||
|
||||
return {
|
||||
title: title ? `${title} | ${defaultTitle}` : defaultTitle,
|
||||
description: metaDescription,
|
||||
authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }],
|
||||
openGraph: {
|
||||
title: title || defaultTitle,
|
||||
description: metaDescription,
|
||||
type: "website",
|
||||
locale: "no",
|
||||
siteName: defaultTitle,
|
||||
images: imageURL
|
||||
? [
|
||||
{
|
||||
url: imageURL,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title || defaultTitle,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
url: `${siteUrl}/logo_2024_neon_trans.png`,
|
||||
alt: defaultTitle,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: imageURL ? "summary_large_image" : "summary",
|
||||
title: title || defaultTitle,
|
||||
description: metaDescription,
|
||||
images: imageURL ? [imageURL] : [`${siteUrl}/logo_2024_neon_trans.png`],
|
||||
},
|
||||
other: {
|
||||
"theme-color": themeColor,
|
||||
"msapplication-TileColor": themeColor,
|
||||
"msapplication-navbutton-color": themeColor,
|
||||
"apple-mobile-web-app-status-bar-style": "black-translucent",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function generateSeoViewport(): Viewport {
|
||||
return {
|
||||
themeColor: "#7d36e7",
|
||||
colorScheme: "dark",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { FaArrowLeft } from "react-icons/fa"
|
||||
|
||||
import * as styles from "./back.module.css"
|
||||
|
||||
const Back = () => {
|
||||
const t = useTranslations("buttons")
|
||||
|
||||
const handleBack = () => {
|
||||
const currentDomain = window.location.hostname
|
||||
const referrer = document.referrer
|
||||
|
||||
const isSameDomain =
|
||||
referrer && new URL(referrer).hostname === currentDomain
|
||||
|
||||
if (window.history.length > 1 && isSameDomain) {
|
||||
window.history.back()
|
||||
} else {
|
||||
window.location.href = "/"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${styles.container} ${styles.button}`}
|
||||
onClick={handleBack}
|
||||
>
|
||||
<FaArrowLeft />
|
||||
<p>{t("back")}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Back
|
||||
@@ -1,12 +1,12 @@
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.6rem;
|
||||
background-color: #5865F2;
|
||||
font-size: 1.6rem;
|
||||
background-color: #5865f2;
|
||||
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 999em;
|
||||
border-radius: 999em;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./discordButton.module.css"
|
||||
|
||||
const DiscordButton = () => (
|
||||
@@ -0,0 +1,20 @@
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
background-color: var(--main-color);
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 999em;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
||||
.button:hover {
|
||||
color: white;
|
||||
background-color: #a76cff;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import * as styles from "./memberButton.module.css"
|
||||
|
||||
const MemberButton = () => {
|
||||
const t = useTranslations("common")
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://innmelding.uiogaming.no"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.button}
|
||||
>
|
||||
{t("join")}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
export default MemberButton
|
||||
@@ -0,0 +1,78 @@
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
justify-content: center;
|
||||
font-size: 1.6rem;
|
||||
background-color: #cccccc;
|
||||
width: 100%;
|
||||
|
||||
padding: 0.5em 1em;
|
||||
border: 0;
|
||||
border-radius: 999em;
|
||||
|
||||
color: black;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dropdownButton {
|
||||
width: 100%;
|
||||
color: black;
|
||||
background-color: #cccccc;
|
||||
border: none;
|
||||
border-radius: 999em !important;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border-radius: 10px;
|
||||
text-align: left;
|
||||
font-size: 1.6rem;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.5em 1em;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.dropdownButton:hover {
|
||||
color: black;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.arrowUp,
|
||||
.arrowDown {
|
||||
border: solid black;
|
||||
border-width: 0 2px 2px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
margin-left: 5px;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.arrowDown {
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.arrowUp {
|
||||
transform: rotate(-135deg);
|
||||
-webkit-transform: rotate(-135deg);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import * as styles from "./otherButton.module.css"
|
||||
|
||||
const OtherButton = () => {
|
||||
const t = useTranslations("about")
|
||||
const tCommon = useTranslations("common")
|
||||
const locale = useLocale()
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const toggleDropdown = () => setIsOpen(!isOpen)
|
||||
|
||||
const statutesUrl =
|
||||
locale === "en"
|
||||
? "https://statutes.uiogaming.no"
|
||||
: "https://vedtekter.uiogaming.no"
|
||||
|
||||
const links = [
|
||||
{ url: statutesUrl, label: tCommon("statutes") },
|
||||
{ url: "https://klage.uiogaming.no", label: tCommon("complaint") },
|
||||
{
|
||||
url: "https://github.com/UiO-Gaming/dokumenter",
|
||||
label: tCommon("documents"),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.dropdownContainer}>
|
||||
<button onClick={toggleDropdown} className={styles.button}>
|
||||
{t("other")}{" "}
|
||||
<span className={isOpen ? styles.arrowUp : styles.arrowDown}></span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className={styles.dropdown}>
|
||||
{links.map((link, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => window.open(link.url, "_blank")}
|
||||
className={styles.dropdownButton}
|
||||
style={{ marginTop: index === 0 ? "8px" : "4px" }}
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OtherButton
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
|
||||
interface UmamiAnalyticsProps {
|
||||
websiteId: string
|
||||
src?: string
|
||||
domains?: string[]
|
||||
}
|
||||
|
||||
export default function UmamiAnalytics({
|
||||
websiteId,
|
||||
src = "https://analytics.umami.is/script.js",
|
||||
domains,
|
||||
}: UmamiAnalyticsProps) {
|
||||
if (!websiteId) {
|
||||
console.warn("Umami: Website ID is required")
|
||||
return null
|
||||
}
|
||||
|
||||
// Only load analytics in production or if explicitly enabled
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return null
|
||||
}
|
||||
|
||||
// Domain validation (client-side check as additional layer)
|
||||
if (domains && typeof window !== "undefined") {
|
||||
const currentDomain = window.location.hostname
|
||||
const isAllowedDomain = domains.some(
|
||||
domain => currentDomain === domain || currentDomain.endsWith(`.${domain}`)
|
||||
)
|
||||
|
||||
if (!isAllowedDomain) {
|
||||
console.warn(
|
||||
`Umami: Domain ${currentDomain} not in allowed list:`,
|
||||
domains
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Script
|
||||
src={src}
|
||||
data-website-id={websiteId}
|
||||
data-domains={domains?.join(",")}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getRequestConfig } from "next-intl/server"
|
||||
import { cookies } from "next/headers"
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
const cookieStore = await cookies()
|
||||
const locale = cookieStore.get("locale")?.value || "no"
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../../messages/${locale}.json`)).default,
|
||||
}
|
||||
})
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="71" height="55" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z" fill="#ffffff"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="71" height="55" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 519 KiB |
|
Before Width: | Height: | Size: 2.9 MiB |
@@ -0,0 +1,29 @@
|
||||
interface AnalyticsConfig {
|
||||
websiteId: string
|
||||
src: string
|
||||
domains: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export function getAnalyticsConfig(): AnalyticsConfig | null {
|
||||
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
const src =
|
||||
process.env.NEXT_PUBLIC_UMAMI_SRC || "https://analytics.umami.is/script.js"
|
||||
|
||||
if (!websiteId) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Define allowed domains
|
||||
const domains = ["uiogaming.no", "www.uiogaming.no"]
|
||||
|
||||
// Additional environment-based checks
|
||||
const enabled = process.env.NODE_ENV === "production"
|
||||
|
||||
return {
|
||||
websiteId,
|
||||
src,
|
||||
domains,
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import imageUrlBuilder from "@sanity/image-url"
|
||||
import type { SanityImageSource } from "@sanity/image-url/lib/types/types"
|
||||
|
||||
import { sanityClient } from "./sanity"
|
||||
|
||||
const builder = imageUrlBuilder(sanityClient)
|
||||
|
||||
export function urlFor(source: SanityImageSource) {
|
||||
return builder.image(source)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createClient } from "@sanity/client"
|
||||
|
||||
export const sanityClient = createClient({
|
||||
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
|
||||
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
|
||||
apiVersion: "2025-02-19",
|
||||
useCdn: true,
|
||||
})
|
||||
|
||||
export async function getBlogPost(slug: string) {
|
||||
const query = `
|
||||
*[_type == "post" && slug.current == $slug][0] {
|
||||
_id,
|
||||
_createdAt,
|
||||
title,
|
||||
author-> {
|
||||
name,
|
||||
image {
|
||||
asset-> {
|
||||
_id,
|
||||
url
|
||||
}
|
||||
}
|
||||
},
|
||||
mainImage {
|
||||
asset-> {
|
||||
_id,
|
||||
url
|
||||
}
|
||||
},
|
||||
excerpt,
|
||||
body
|
||||
}
|
||||
`
|
||||
|
||||
return sanityClient.fetch(
|
||||
query,
|
||||
{ slug },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: ["blog-posts", `post-${slug}`],
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function getEvent(slug: string) {
|
||||
const query = `
|
||||
*[_type == "event" && slug.current == $slug][0] {
|
||||
_id,
|
||||
title,
|
||||
location,
|
||||
date,
|
||||
slug,
|
||||
description
|
||||
}
|
||||
`
|
||||
|
||||
return sanityClient.fetch(
|
||||
query,
|
||||
{ slug },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: ["events", `event-${slug}`],
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function getAllEvents() {
|
||||
const query = `
|
||||
*[_type == "event"] | order(date asc) {
|
||||
_id,
|
||||
title,
|
||||
location,
|
||||
date,
|
||||
slug,
|
||||
description
|
||||
}
|
||||
`
|
||||
|
||||
return sanityClient.fetch(
|
||||
query,
|
||||
{},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: ["events"],
|
||||
revalidate: 1800, // 30 minutes
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./404.module.css"
|
||||
|
||||
import { Link } from "gatsby"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import Seo from "../components/seo"
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout>
|
||||
<Seo title="404: Not found" />
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.centeredText}>404: Siden finnes ikke</h1>
|
||||
<p className={styles.centeredText}>
|
||||
Du har nådd en blindvei, men ikke på Blindern{" "}
|
||||
<span role="img" aria-label="eyes">
|
||||
👀
|
||||
</span>
|
||||
</p>
|
||||
<Link to="/" className={`${styles.centeredText} ${styles.homeLink}`}>
|
||||
Tilbake til hjemmesiden
|
||||
</Link>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFoundPage
|
||||
@@ -1,80 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./blog.module.css"
|
||||
|
||||
import { graphql, Link } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import Seo from "../components/seo"
|
||||
import Back from "../components/items/back"
|
||||
|
||||
export const query = graphql`
|
||||
query {
|
||||
allSanityPost(sort: { fields: _createdAt, order: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
_createdAt(formatString: "DD. MMMM, YYYY", locale: "nb_NO")
|
||||
_id
|
||||
slug {
|
||||
current
|
||||
}
|
||||
excerpt
|
||||
author {
|
||||
name
|
||||
image {
|
||||
asset {
|
||||
gatsbyImageData(width: 40, height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
mainImage {
|
||||
asset {
|
||||
gatsbyImageData(aspectRatio: 1.618)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const BlogPage = ({ data }) => (
|
||||
<Layout>
|
||||
<Seo
|
||||
title="Blog"
|
||||
description="Nyheter om hva som skjer i foreningen og skriverier om alt og ingenting"
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.back}>
|
||||
<Back />
|
||||
</div>
|
||||
<div className={styles.articleContainer}>
|
||||
{data.allSanityPost.edges.map(post => (
|
||||
<Link to={post.node.slug.current} className={styles.link}>
|
||||
<article className={styles.card}>
|
||||
<GatsbyImage
|
||||
image={post.node.mainImage.asset.gatsbyImageData}
|
||||
className={styles.headerImage}
|
||||
/>
|
||||
<div className={styles.cardContent}>
|
||||
<h2 className={styles.header}>{post.node.title}</h2>
|
||||
<p className={styles.published}>{post.node._createdAt}</p>
|
||||
<div className={styles.author}>
|
||||
<GatsbyImage
|
||||
className={`circular ${styles.authorImage}`}
|
||||
alt={"Forfatteren av innlegget, " + post.node.author.name}
|
||||
image={post.node.author.image.asset.gatsbyImageData}
|
||||
/>
|
||||
<p>{post.node.author.name}</p>
|
||||
</div>
|
||||
<p>{post.node.excerpt}</p>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default BlogPage
|
||||
@@ -1,66 +0,0 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
.back {
|
||||
justify-self: start;
|
||||
}
|
||||
.articleContainer {
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
place-items: center;
|
||||
align-items: stretch;
|
||||
gap: 3rem;
|
||||
}
|
||||
.card {
|
||||
display: grid;
|
||||
max-width: 50ch;
|
||||
box-shadow: 0 6px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.headerImage {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 2rem;
|
||||
}
|
||||
.published {
|
||||
font-weight: 100;
|
||||
}
|
||||
.header {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
font-weight: 100;
|
||||
margin-block: 1rem;
|
||||
}
|
||||
.authorImage {
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: white;
|
||||
}
|
||||
.link h2 {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--link-color);
|
||||
}
|
||||
.link:hover h2 {
|
||||
text-decoration-color: var(--link-color-hover);
|
||||
}
|
||||
|
||||
@media (min-width: 60em) {
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
.articleContainer {
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./blogPost.module.css"
|
||||
|
||||
import { graphql } from "gatsby"
|
||||
import { GatsbyImage } from "gatsby-plugin-image"
|
||||
|
||||
import Seo from "../components/seo"
|
||||
import BlockContent from "@sanity/block-content-to-react"
|
||||
import Back from "../components/items/back"
|
||||
|
||||
export const query = graphql`
|
||||
query($id: String) {
|
||||
sanityPost(_id: { eq: $id }) {
|
||||
_createdAt(formatString: "DD. MMMM, YYYY", locale: "nb_NO")
|
||||
title
|
||||
author {
|
||||
name
|
||||
image {
|
||||
asset {
|
||||
gatsbyImageData(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
mainImage {
|
||||
asset {
|
||||
url
|
||||
gatsbyImageData
|
||||
}
|
||||
}
|
||||
excerpt
|
||||
_rawBody(resolveReferences: { maxDepth: 10 })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const BlogPost = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<Seo
|
||||
title={data.sanityPost.title}
|
||||
imageURL={data.sanityPost.mainImage.asset.url}
|
||||
description={data.sanityPost.excerpt}
|
||||
author={data.sanityPost.author.name}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.backButton}>
|
||||
<Back />
|
||||
</div>
|
||||
<article className={styles.article}>
|
||||
<header className={styles.header}>
|
||||
<GatsbyImage
|
||||
image={data.sanityPost.mainImage.asset.gatsbyImageData}
|
||||
className={styles.headerImage}
|
||||
/>
|
||||
<h1 className={styles.title}>{data.sanityPost.title}</h1>
|
||||
<p className={styles.excerpt}>{data.sanityPost.excerpt}</p>
|
||||
<div className={styles.author}>
|
||||
<GatsbyImage
|
||||
className="circular"
|
||||
alt={"Forfatteren av innlegget, " + data.sanityPost.author.name}
|
||||
image={data.sanityPost.author.image.asset.gatsbyImageData}
|
||||
/>
|
||||
<div className={styles.authorNameSection}>
|
||||
<p>{data.sanityPost.author.name}</p>
|
||||
<p>{data.sanityPost._createdAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<BlockContent
|
||||
className={styles.content}
|
||||
blocks={data.sanityPost._rawBody}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
@@ -1,61 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./eventPost.module.css"
|
||||
|
||||
import { graphql } from "gatsby"
|
||||
import moment from "moment-timezone"
|
||||
|
||||
import Seo from "../components/seo"
|
||||
import Back from "../components/items/back"
|
||||
import { FaMapMarkerAlt, FaCalendarAlt } from "react-icons/fa"
|
||||
|
||||
export const query = graphql`
|
||||
query($id: String) {
|
||||
sanityEvent(_id: { eq: $id }) {
|
||||
title
|
||||
location
|
||||
date
|
||||
slug {
|
||||
current
|
||||
}
|
||||
description
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
moment.locale("nb_NO")
|
||||
|
||||
const Event = ({ data }) => {
|
||||
const time = moment(data.sanityEvent.date)
|
||||
.tz("Europe/Oslo")
|
||||
.format("dddd Do MMMM, [kl.] H:mm")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Seo
|
||||
title={data.sanityEvent.title}
|
||||
description={`Tidspunkt: ${time}\nSted: ${data.sanityEvent.location}\n\n${data.sanityEvent.description}`}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<Back />
|
||||
<div className={styles.event}>
|
||||
<h1>{data.sanityEvent.title}</h1>
|
||||
<div className={styles.metadata}>
|
||||
<FaMapMarkerAlt />
|
||||
<p>{data.sanityEvent.location}</p>
|
||||
</div>
|
||||
<div className={styles.metadata}>
|
||||
<FaCalendarAlt />
|
||||
<p>
|
||||
{moment(data.sanityEvent.date)
|
||||
.tz("Europe/Oslo")
|
||||
.format("dddd Do MMMM, [kl.] H:mm")}
|
||||
</p>
|
||||
</div>
|
||||
<p className={styles.description}>{data.sanityEvent.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Event
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from "react"
|
||||
import "../index.css"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import Seo from "../components/seo"
|
||||
|
||||
import Header from "../components/sections/header"
|
||||
import About from "../components/sections/about"
|
||||
import Events from "../components/sections/events"
|
||||
import Office from "../components/sections/office"
|
||||
import BoardMembers from "../components/sections/boardMembers"
|
||||
import Contact from "../components/sections/contact"
|
||||
import Footer from "../components/sections/footer"
|
||||
|
||||
const IndexPage = () => (
|
||||
<Layout>
|
||||
<Seo />
|
||||
<Header />
|
||||
<About />
|
||||
<Events />
|
||||
<Office />
|
||||
<BoardMembers />
|
||||
<Contact />
|
||||
<Footer />
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default IndexPage
|
||||
@@ -1,33 +0,0 @@
|
||||
import * as React from "react"
|
||||
import * as styles from "./room.module.css"
|
||||
|
||||
import Layout from "../components/layout"
|
||||
import Seo from "../components/seo"
|
||||
|
||||
const RoomPage = () => (
|
||||
<Layout>
|
||||
<Seo
|
||||
title="Foreningsrom"
|
||||
description="Kalender og booking av foreningsrom"
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<iframe
|
||||
src="https://calendar.google.com/calendar/embed?height=600&wkst=2&bgcolor=%23D50000&ctz=Europe%2FBerlin&showPrint=0&showTabs=1&showCalendars=0&showTz=1&mode=WEEK&showTitle=0&title=Rombooking&src=N2YxM2IzYWIyNDA3NTEyZGU3ZDk2MTdhZmNkZmI2ZjFmZjg1YzU3N2QwODNjZGIxNzA3MWRiNTdlMmQ3ZDM1NEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&color=%233F51B5"
|
||||
width="800"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
></iframe>
|
||||
<a
|
||||
href="https://forms.gle/1aj4yKXpbb788sWo8"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.button}
|
||||
>
|
||||
Book rommet
|
||||
</a>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default RoomPage
|
||||
@@ -1,22 +0,0 @@
|
||||
.container {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 1.6rem;
|
||||
background-color: var(--main-color);
|
||||
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 999em;
|
||||
|
||||
color: var(--text-color);
|
||||
}
|
||||
.button:hover {
|
||||
color: white;
|
||||
background-color: rgb(255, 62, 52);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@lib/*": ["./src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"global.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
const express = require("express")
|
||||
const cors = require("cors")
|
||||
|
||||
const { exec } = require("child_process")
|
||||
|
||||
const app = express()
|
||||
const frontendURL = process.env.FRONTEND_URL || "https://uiogaming.no"
|
||||
const port = process.env.PORT || 9001
|
||||
|
||||
app.use(cors({ origin: frontendURL }))
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send("API is running! Yeet!")
|
||||
})
|
||||
|
||||
app.post("/build", (req, res) => {
|
||||
exec("cd ../ && gatsby build", (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(stderr.message)
|
||||
console.log(error.message)
|
||||
return
|
||||
}
|
||||
console.log(stdout)
|
||||
})
|
||||
|
||||
res.send("Success!")
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`LIVE! http://0.0.0.0:${port}`)
|
||||
})
|
||||