165 Commits
Author SHA1 Message Date
leanderandClaude Opus 5.5 9a8076813b ci: deploy through the restricted uiogaming-deploy script
The old workflow ran docker compose in the repo folder, which failed because the containers belong to the server's top-level compose project. The deploy key can now only run /usr/local/bin/uiogaming-deploy (see UiO-Gaming/drift), so the workflow just passes the service name. Also adds manual runs and prevents overlapping deploys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:47:58 +01:00
leander d9efb8c51f Merge pull request #56 from UiO-Gaming/release-please--branches--main--components--uiogaming.no
chore(main): release uiogaming.no 1.0.1
2026-11-02 22:19:45 +01:00
github-actions[bot] a89c2f609d chore(main): release uiogaming.no 1.0.1 2026-11-02 21:18:29 +00:00
leander 15982ed804 Merge branch 'slop/sloppy-fixes'
# Conflicts:
#	README.md
2026-11-02 22:15:02 +01:00
leanderandClaude Opus 5.5 d20de4b455 ci: add release-please for automated releases
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:56:49 +01:00
leander a09b7ae871 elaborate on note about webhook 2026-11-01 22:24:20 +01:00
leanderandClaude Opus 5.5 1a18096cf6 Apply Sanity crop and hotspot to images
Fetch the full image objects instead of only the asset URL so the URL
builder can apply the crop and hotspot set in the Studio, crop the Open
Graph image around the hotspot, and add sizes to full-width blog images.

Closes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:42:21 +01:00
leanderandClaude Opus 5.5 f647dd869d Update contributing guide for the bilingual site and new checks
Text shown to users goes in both message files instead of being
Norwegian only. Document lint and CI, and make npm run format cover
the same files as the CI check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:05:50 +01:00
leander 28346d6907 fix typos 2026-10-30 23:03:09 +01:00
leanderandClaude Opus 5.5 b60f8a92e1 Add sitemap.xml and robots.txt
List the home page, blog posts and events in both languages with
hreflang alternates, and point crawlers to the sitemap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:01:49 +01:00
leanderandClaude Opus 5.5 2e54e32ea6 Add canonical URLs, hreflang alternates and article metadata
Set metadataBase so relative metadata URLs resolve to the real domain,
link the Norwegian and English versions of each page, and mark blog
posts as articles with their publish date and author.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:59:56 +01:00
leanderandClaude Opus 5.5 115afd4275 Make the Back button aware of client-side navigation
document.referrer only changes on full page loads, so the button sent
visitors to the home page instead of back after navigating within the
site. Use the Navigation API's canGoBack, which only considers this
site's history entries, and keep the referrer check as a fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:57:28 +01:00
leanderandClaude Opus 5.5 67641c5719 Don't open mailto: and internal links in a new tab
target="_blank" on mailto: links leaves an empty tab behind. In blog
posts, only links to other sites now open in a new tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:56:08 +01:00
leanderandClaude Opus 5.5 70ba50c515 Hide decorative flags from screen readers in the language selector
The flags had titles that repeated the button text. Also mark each
language name with its own lang so it's pronounced correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:54:18 +01:00
leanderandClaude Opus 5.5 633237f262 Make the "Other" dropdown accessible
Expose its state with aria-expanded, close it on outside clicks,
Escape and when focus leaves, restore the focus outline, and move the
inline margins into CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:53:03 +01:00
leanderandClaude Opus 5.5 eab0e0bbab Use a span instead of a paragraph inside the Back button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:51:56 +01:00
leanderandClaude Opus 5.5 7822daa557 Add CI workflow that checks pull requests
Run formatting, lint, type checking and a production build on every
pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:51:03 +01:00
leanderandClaude Opus 5.5 773d21b709 Shorten deploy downtime and limit workflow permissions
Build the new image before replacing the running container instead of
stopping the site first, prune old images, prevent overlapping deploys,
and drop the unused pull-requests permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:40:21 +01:00
leanderandClaude Opus 5.5 b8e352ba22 Use a multi-stage Docker build with Next's standalone output
Install with npm ci, ship only the standalone server and its traced
dependencies, and run as a non-root user. Shrinks the image from
1.78 GB to 438 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:39:14 +01:00
leanderandClaude Opus 5.5 703ab5a32c Only send translations used by client components to the browser
Server components read their translations on the server, but every
namespace was serialized into each page for the client provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:30:41 +01:00
leanderandClaude Opus 5.5 edf0eed3dc Lazy-load the MazeMap iframe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:28:03 +01:00
leanderandClaude Opus 5.5 1d506a7b4c Fix deprecated Prettier option and make the repo pass prettier --check
jsxBracketSameLine was replaced by bracketSameLine in Prettier 3.
Ignore tsconfig.json, since Next rewrites it in its own format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:27:37 +01:00
leanderandClaude Opus 5.5 aca183a4a2 Use the @/ import alias consistently
Drop the @components and @lib aliases, which only a few imports used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:26:06 +01:00
leanderandClaude Opus 5.5 3a0ad44e06 Define icons, theme color and viewport once
Icons were set in both the layout metadata and every page's metadata,
and theme-color was output twice. Inherit icons from the layout and
export the viewport from the layout instead of from each page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:25:05 +01:00
leanderandClaude Opus 5.5 0eb47846e7 Simplify Umami analytics into one server component
The config was split between lib/analyticsConfig and a client component
that repeated the production check and default URL, and checked window
during render. Umami's data-domains already restricts tracking to our
domains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:23:06 +01:00
leanderandClaude Opus 5.5 5c612d995d Share event date handling and stop depending on the server timezone
Event dates are stored as Oslo wall-clock strings, but were parsed in
the server's local timezone, so times were only correct because the
container sets TZ=Europe/Oslo. Move date parsing and formatting into
lib/dates, interpret those strings as Oslo time, and also accept ISO
timestamps in case the Studio writes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:20:46 +01:00
leanderandClaude Opus 5.5 35c175c1ad Remove unused translation key and query field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:18:02 +01:00
leanderandClaude Opus 5.5 19cda1b817 Remove accidentally added install package
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:16:24 +01:00
leanderandClaude Opus 5.5 7c344a2c74 Rename middleware.ts to proxy.ts
The middleware file convention is deprecated in Next 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:15:24 +01:00
leanderandClaude Opus 5.5 5b135ce712 Define locales once with next-intl's routing config
Locales were listed separately in the middleware, the request config
and several type casts. Use defineRouting and createNavigation, type
the Locale globally, and let the language selector use the locale-aware
router so it keeps the query string and hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:13:57 +01:00
leander b84781ebe5 remove unused i18n request config 2026-10-30 22:10:42 +01:00
leanderandClaude Opus 5.5 aca584791e Update Node to 26 and dependencies to their latest versions
- Node 20 is end-of-life; use the current LTS in the Docker image
- Major updates: @sanity/client 8, @sanity/image-url 2,
  @portabletext/react 8, eslint-config-next 16
- Unpin React and update to 19.3
- ESLint stays on 9 and TypeScript on 6.0, since eslint-config-next's
  plugins and typescript-eslint don't support newer majors yet
- Use the native flat config from eslint-config-next 16
- Use useSyncExternalStore for WebGL detection to satisfy the new
  react-hooks lint rule

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:01:56 +01:00
leanderandClaude Opus 5.5 a1729494b4 Only render safe link URLs from blog content
Blog links come from the CMS, so a javascript: URL would run code when
clicked. Allow only http(s), mailto and relative links, and render
anything else as plain text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:51:53 +01:00
leanderandClaude Opus 5.5 d04afb3e0c Use real links in the "Other" dropdown
The items were buttons calling window.open() without noopener, which
let the opened page access window.opener. Links also support
middle-click and show the URL on hover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:50:23 +01:00
leanderandClaude Opus 5.5 c997b74d01 Verify Sanity webhook signatures in the revalidate route
The secret was passed in the query string, which ends up in access logs
and doesn't protect the request body. Verify the signed header with
@sanity/webhook instead, and return 400 for invalid JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:48:49 +01:00
leanderandClaude Opus 5.5 c5c366884a Fetch only upcoming events from Sanity
Filter and limit in GROQ instead of fetching every event and filtering
in JavaScript. Event dates are stored as Oslo local time strings, so
compare them against the current Oslo time in the same format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:46:06 +01:00
leanderandClaude Opus 5.5 b05ecf757d Handle events without a description
Event cards crashed on a missing description, and the event metadata
included "null" for missing fields. Truncate previews by grapheme so
emoji aren't split, and show descriptions of exactly 70 characters in
full.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:41:07 +01:00
leanderandClaude Opus 5.5 1f82345a2f Keep .env and local build artifacts out of the Docker image
Without a .dockerignore, COPY . . baked the .env file (including the
webhook secret) into the image and overwrote the container's
node_modules with the host's. Pass public build-time values as build
args and provide the rest at runtime through env_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:38:49 +01:00
leander d03278ef0d replace deprecated next lint with eslint 2026-10-30 21:30:12 +01:00
leanderandClaude Opus 5.5 da3d26d16b Render pages statically using next/root-params
getLocale() read request headers, which made every route dynamic and the
page-level revalidate a no-op. Make [locale]/layout.tsx the root layout
so the locale can be read through next/root-params, set <html lang>, add
generateStaticParams for locales and slugs, and pass the locale
explicitly to metadata translations.

The middleware now matches all non-file paths so URLs without a locale
are redirected instead of hitting the root layout's notFound().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:28:32 +01:00
leanderandClaude Opus 5.5 a8b31a5379 Expire cache tags immediately on webhook revalidation
The "max" profile serves stale content once more while revalidating, so
the first visit after publishing showed the old version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:18:07 +01:00
leander 30da54c78e use next cache instead of sanity's cache 2026-10-30 21:16:08 +01:00
leander 13463d4eeb fetch board member data on data change 2026-10-30 21:13:58 +01:00
leander d44fbfa9d7 fix 404 page i18n handling 2026-10-30 21:11:53 +01:00
leander 52b8f2b7b7 remove redundant layout tags 2026-10-30 21:03:53 +01:00
leander 38f1ba69b4 return 404 on no data for blogposts/events 2026-10-30 21:02:32 +01:00
leander 39ad3947fd run npm audit fix 2026-10-30 15:49:10 +01:00
leander 14c1403e6d fix some vulns 2026-10-22 19:04:06 +02:00
leander e238eed619 clarify webhook api deployment 2026-09-10 02:17:26 +02:00
leander 08bc43942a fix vulns 2026-08-29 16:47:38 +02:00
leander b0e343a390 Merge pull request #55 from UiO-Gaming/dependabot/npm_and_yarn/multi-5a59400ff4
Bump brace-expansion
2026-08-28 21:08:07 +02:00
leander c7d646362b Merge pull request #54 from UiO-Gaming/dependabot/npm_and_yarn/next-15.5.24
Bump next from 15.5.18 to 15.5.24
2026-08-28 21:07:32 +02:00
dependabot[bot] 2183175de6 Bump brace-expansion
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 1.1.14 to 1.1.18
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.18)

Updates `brace-expansion` from 2.1.0 to 2.1.4
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.14...v1.1.18)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 2.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 19:05:43 +00:00
dependabot[bot] bf6f282155 Bump next from 15.5.18 to 15.5.24
Bumps [next](https://github.com/vercel/next.js) from 15.5.18 to 15.5.24.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.18...v15.5.24)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.24
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 19:04:30 +00:00
leander 8480327a4c Merge pull request #53 from UiO-Gaming/dependabot/npm_and_yarn/next-15.5.18
Bump next from 15.5.15 to 15.5.18
2026-05-14 22:35:19 +08:00
dependabot[bot] 30abf637ac Bump next from 15.5.15 to 15.5.18
Bumps [next](https://github.com/vercel/next.js) from 15.5.15 to 15.5.18.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.15...v15.5.18)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.18
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-14 13:45:01 +00:00
leander 93b2dc4d4e fix vulns 2026-04-14 04:25:01 +02:00
leander 183e519a19 fix CVE-2026-27903 2026-03-23 15:14:15 +01:00
leander 1c02352aad add section ids
allow linkink to certain parts of the homepage
2026-03-09 21:03:48 +01:00
leander abcc39ce6d fix GHSA-h25m-26qc-wcjf 2026-02-14 15:41:52 +01:00
leander e9cff6ab84 another day, another vuln to fix 2026-01-19 02:08:14 +01:00
leander 6cc19ea4fb LET ME ENJOY MY VACATION IN PEACE STUPID FUCKING FRAMEWORK 2025-12-13 03:28:28 +01:00
leander 743df3691c Merge pull request #52 from UiO-Gaming/dependabot/npm_and_yarn/multi-79dce8efd9
Bump glob and npm
2025-12-13 02:24:34 +00:00
dependabot[bot] afa4e5580c Bump glob and npm
Removes [glob](https://github.com/isaacs/node-glob). It's no longer used after updating ancestor dependency [npm](https://github.com/npm/cli). These dependencies need to be updated together.


Removes `glob`

Updates `npm` from 11.5.2 to 11.7.0
- [Release notes](https://github.com/npm/cli/releases)
- [Changelog](https://github.com/npm/cli/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/cli/compare/v11.5.2...v11.7.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 
  dependency-type: indirect
- dependency-name: npm
  dependency-version: 11.7.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-13 02:24:04 +00:00
leander 8a7ecf223d everyday I wake up to a new js-something vuln and I'm sick and tired of it
let me vacation in peacegit add .git add .
2025-12-09 12:58:09 +01:00
leander 3c6c55afd9 use padding instead of margin for event page spacing 2025-09-16 04:20:03 +02:00
leander 38d015fb20 add left margin to back button 2025-09-16 04:16:29 +02:00
leander d871eee968 add larger top and bottom margin to event pages 2025-09-16 04:13:30 +02:00
leander 2591eacd14 Merge pull request #51 from UiO-Gaming/fix/translation-rewrite
fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple fuck apple
2025-09-16 04:09:47 +02:00
leander 017f5154b1 Rewrite translation logic
Rerendering translations apparently caused issues with safari because
apple fucking sucks. Thus we had to rewrite the logic to use
prefix-based translations. This is the attempt at this. It might be a
mega commit but it's how it ought to be...
2025-09-16 03:31:11 +02:00
leander 1434528e4f add timezone to docker image 2025-08-24 19:14:18 +02:00
leander 0e283cc794 add pc room image alt text translations 2025-08-21 04:27:34 +02:00
leander e04422acb4 change phrasing of office description 2025-08-21 04:23:45 +02:00
leander 30b49ee8c9 add timezone to next-intl request middleware 2025-08-21 04:01:03 +02:00
leander 267bce6422 new 3d office + pc room image 2025-08-21 04:01:03 +02:00
leander 540ce583c9 add mazemap iframe with office directions 2025-08-11 14:44:58 +02:00
leander 48ff006c13 add restart clause to docker compose 2025-08-07 20:27:16 +02:00
leander 0fc5a7a7b6 move footer orginfo to center of grid container 2025-08-06 01:19:55 +02:00
leander 864aef1b7b make dropdown menu buttons at least the length of the parent button 2025-08-06 01:17:43 +02:00
leander 50bdf6503e reintroduce translation support 2025-08-06 01:16:40 +02:00
leander b4bb073d87 minor event image width increase 2025-08-01 04:44:51 +02:00
leander 8e7f4b7659 make a mess and add max width to footer 2025-08-01 04:32:38 +02:00
leander b00debdbb0 make dropdown buttons thinner 2025-08-01 04:07:23 +02:00
leander bd6012b0ab fix(?) event image size 2025-08-01 03:59:55 +02:00
leander d313f903ee fix image alignment for about section 2025-08-01 03:45:47 +02:00
leander d9155dec10 fix fallback board member image handler 2025-08-01 03:30:00 +02:00
leander 95f75d9bb1 fix: photo credit text placement 2025-08-01 00:30:54 +02:00
leander d9a6019c7c reintroduce umami analytics support 2025-08-01 00:19:35 +02:00
leander ae59bfeb5f create deploy action 2025-07-31 23:36:45 +02:00
leander 171a3053f3 add image of website to readme 2025-07-31 22:56:51 +02:00
leander ee32c1723b Add docker support
Closes #31
2025-07-31 22:47:49 +02:00
leander e2d2836e69 Merge pull request #49 from UiO-Gaming/nextjs-rewrite
the big mega huge amazing nextjs migration
2025-07-31 23:41:16 +03:00
leander e7d7e29edf the big mega huge amazing nextjs migration
Code still trash though but we'll fix it someday

Closes #48, #47, #37, #32
2025-07-31 22:38:27 +02:00
leander e07e6ebcd0 remove deploy workflow 2025-07-22 02:06:28 +02:00
leander 0a3894e14d temporarily remove english translation
the site would not build due to some weird issue I have yet to figure
out. temporarily removing all english translations fixes it however. I
am probably migrating the site away from gatsby anyway so a temp fix is
fine
2025-07-22 02:05:19 +02:00
leander 626d749611 move action into the correct folder 2025-07-10 14:36:22 +02:00
leander cf84a5086e add deploy action 2025-07-10 14:34:33 +02:00
leander f2d7b15c4e stretch board member to same height 2025-07-02 21:35:06 +02:00
leander 5bd7321558 upgrade dependencies 2025-02-17 12:51:03 +01:00
Leander Furumo 400af2fb31 upgrade webhook-api dependencies 2025-02-12 12:47:12 +01:00
Leander Furumo c7185a79f3 upgrade dependencies 2025-02-12 12:45:30 +01:00
leander 2c6e4c2412 add tiktok link to contact section 2025-02-07 16:18:36 +01:00
leander 31a4b1cf81 reorder social links by importance 2025-02-07 16:16:29 +01:00
leander ef764a4b8d finish reverting npm migration 2025-02-07 16:06:54 +01:00
leander d21053b544 Revert "migrate webhook-api from yarn to npm"
This reverts commit dca3c08ee1.
2025-02-07 16:05:49 +01:00
leander 828468e70e Revert "make documentation reflect yarn to npm migration"
This reverts commit 7d17abc2c5.
2025-02-07 16:00:53 +01:00
leander 3d798a8913 Revert "migrate from yarn to npm"
This reverts commit 42c75194ed.
2025-02-07 16:00:23 +01:00
leander 7d17abc2c5 make documentation reflect yarn to npm migration 2025-01-16 12:13:51 +01:00
leander dff4669321 add rate limiting to webhook-api 2025-01-16 12:13:04 +01:00
leander dca3c08ee1 migrate webhook-api from yarn to npm 2025-01-16 12:11:36 +01:00
leander 42c75194ed migrate from yarn to npm 2025-01-16 12:00:04 +01:00
leander ead41eeaea upgrade dependencies 2024-11-07 19:22:28 +01:00
leander d20022899a reintroduce umami script 2024-11-07 19:15:04 +01:00
leander e0735a5c97 update 3D picture of office 2024-11-04 00:07:51 +01:00
leander cb7cdb8128 link using new repo name 2024-10-25 19:09:46 +02:00
leander 36ea11441f modify author 2024-10-25 19:08:46 +02:00
leander fda4846200 add codeowners file 2024-10-25 19:08:39 +02:00
leander 91dc9542e4 I give up
fuck javascript fuck javascript fuck javascript. nodejs was a mistake
2024-10-23 19:00:50 +02:00
leander 0e65652e76 attempt running nvm
fuck ubuntu and its outdated ass node version
2024-10-23 18:51:13 +02:00
leander 8a869a10eb install dependencies before running build on deploy
this is in case we introduce new deps
2024-10-23 17:26:58 +02:00
leander c0200f67f2 add auto-deploy workflow
hopefully it works. probably not knowing my luck
2024-10-23 16:48:23 +02:00
leander 26e2e75765 remove header text
looks ugly
2024-10-23 09:11:23 +02:00
leander d5131824d9 replace logo with 2024 rendition
might have to adjust the sizing later
2024-10-23 09:10:50 +02:00
centauridesigns 9394f1f7b4 minor css change for "other" button
hover behavior changed
2024-05-02 19:04:35 +02:00
LBlend 0f5c7306d3 use custom subdomain redirect for complaints 2024-05-02 13:40:55 +02:00
centauridesigns 9f7dacfb21 update "other" button for statutes 2024-05-01 23:21:08 +02:00
centauridesigns ca81402a45 add "other" button dropdown
new "other" dropdown which can be used for extra member-specific information that may be too important to restrict to footer. hovers are still overwritten, so the gray other button will hover purple. can be fixed later.
2024-05-01 22:33:21 +02:00
LBlend 39dfeb21a3 add senetence detailing our office gear 2024-04-19 02:49:47 +02:00
LBlend 3184215135 tell users about 3D pic functionality 2024-04-19 02:41:18 +02:00
LBlend 75c59eb8bb add new office 3D pic april 2024 2024-04-19 02:32:04 +02:00
LBlend 1e2444150b correct norwegian email translation 2024-03-18 14:42:35 +01:00
centauridesigns 51a58f5c55 remove logging and swap to i18n langauge handler 2024-03-15 16:41:29 +01:00
centauridesigns 9ff744e5b1 add translation support for cms board members
roles and bios are now also available in english.
2024-03-15 16:37:43 +01:00
LBlend d28db43afd translate eventpost metadescription 2024-03-14 16:11:58 +01:00
LBlend d7ad18c60f refactor datetime handling & format based on lang
Date format should now be rendered according to the language. Removed 'moment' dependency as we can use built in js to handle timezones as well as formatting. This is one of the tasks in issue #45
2024-03-14 14:45:23 +01:00
LBlend 6dacc4e5b5 translate metadata 2024-03-14 13:01:29 +01:00
LBlend 568b0524e0 add seo translation files 2024-03-14 13:00:14 +01:00
LBlend 1c7ca69a81 add addressRegion field to google rich snippet 2024-03-14 12:09:00 +01:00
LBlend 89d0e6ab9c add google rich snippet 2024-03-14 12:04:55 +01:00
LBlend c29e9d8f57 change statutes to english version for the english page 2024-03-14 01:20:23 +01:00
LBlend aec4a9406e initial translation support
I should've divided this into multiple commits but oh well. This is just initial as there are still parts that needs to be done. Particularly user generated content needs to be translated somehow. See issue #45 for more info
2024-03-13 02:36:31 +01:00
LBlend 224847b1a7 remove blog overview and room booking page 2024-03-13 02:32:52 +01:00
LBlend dc5ac4b2c7 migrate graphql queries to v3 2024-03-04 23:26:27 +01:00
LBlend 232f69472c change email field to contact info
Giving up your email shouldn't be necessary. Therefore, I have decided to change it to a general contact info field. This change is to simply reflect variable naming of the backend
2024-03-04 16:23:06 +01:00
LBlend 0a62a61ccf make board member email optional 2024-02-07 03:47:56 +01:00
LBlend 2857cc7048 upgrade dependencies 2024-02-07 03:39:26 +01:00
LBlend 37c9618ad6 fix unknown variable name issue 2024-02-07 03:35:53 +01:00
LBlend a9c250063c semantic variable naming 2024-02-07 03:11:08 +01:00
LBlend e2367eb29e change alt text 2024-02-07 03:08:18 +01:00
LBlend 8097ea89cd remove umami 2024-02-07 02:53:45 +01:00
LBlend 3d5c9d4bd9 add photo credit 2024-02-07 02:47:46 +01:00
LBlend fd5d8d1cf1 replace about and event section picture 2024-02-07 02:47:34 +01:00
LBlend 13c0eab15c close #43 2024-02-07 01:10:25 +01:00
LBlend 88df17818c change link selection color 2024-02-07 00:22:28 +01:00
Leander Furumo 269c041a7b Merge pull request #44 from UiO-Gaming/graphic-design-dev
Purple "mustafa" graphic design update
2024-02-06 19:52:51 +01:00
LBlend 6ebe7565c7 bump version number
oopsie woopsie I forgor
2024-02-06 19:48:57 +01:00
LBlend bb9bf87bf5 slightly widen card max width
due to the new font text is wider. meaning an increased chance of newlines. looks weird if not handled
2024-02-06 19:47:36 +01:00
LBlend 83d5088e92 increase name and role contrast 2024-02-06 19:45:43 +01:00
LBlend 5b7d00cee3 increase contrast between location/time text and title 2024-02-06 19:44:56 +01:00
LBlend 53952c614c increase header title and text size 2024-02-06 19:41:40 +01:00
LBlend a33ffce59a rename header background image 2024-02-06 19:40:56 +01:00
LBlend 11b872dbb8 remove unused images 2024-02-06 19:40:16 +01:00
LBlend f72f351618 change embed color 2024-02-06 19:39:07 +01:00
LBlend 33aab96bc7 change author email 2024-02-06 19:38:30 +01:00
LBlend 01e57edc41 first prototype - new graphic design 2024-02-01 21:42:55 +01:00
120 changed files with 9527 additions and 13555 deletions

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
# Installed and built inside the image
node_modules
.next
# Secrets are passed in by docker compose, never baked into the image
.env*
.git
.github
Dockerfile
docker-compose.yml
*.md
npm-debug.log*
+1
View File
@@ -0,0 +1 @@
* @LBlend @centauridesigns
+40
View File
@@ -0,0 +1,40 @@
name: CI
on:
pull_request:
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
# Public values from env.example. The build prerenders pages with data
# from Sanity, so it needs to know which project to read from
env:
NEXT_PUBLIC_SANITY_PROJECT_ID: mmqlu667
NEXT_PUBLIC_SANITY_DATASET: production
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
cache: npm
- run: npm ci
- name: Check formatting
run: npx prettier --check .
- name: Lint
run: npm run lint
# Generates the route and next/root-params types tsc relies on
- name: Type check
run: npx next typegen && npx tsc --noEmit
- name: Build
run: npm run build
+32
View File
@@ -0,0 +1,32 @@
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
# Never run two deploys of this repo at the same time
concurrency: deploy
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Set up SSH
run: |
install -m 600 -D /dev/null ~/.ssh/id_ed25519
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
ssh-keyscan -H ${{ vars.SSH_HOST }} > ~/.ssh/known_hosts
# The deploy key can only run /usr/local/bin/uiogaming-deploy on the server. It takes the
# service name from the top-level docker-compose.yml (see drift/driftsdokumentasjon.md).
- name: Deploy
run: ssh ${{ secrets.SSH_USER }}@${{ vars.SSH_HOST }} uiogaming.no
- name: Clean up
if: always()
run: rm -rf ~/.ssh
+19
View File
@@ -0,0 +1,19 @@
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
+34 -62
View File
@@ -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
+1
View File
@@ -2,3 +2,4 @@
package.json
package-lock.json
public
tsconfig.json
+1 -1
View File
@@ -7,6 +7,6 @@
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"bracketSameLine": false,
"arrowParens": "avoid"
}
+3
View File
@@ -0,0 +1,3 @@
{
".": "1.0.1"
}
+9
View File
@@ -0,0 +1,9 @@
# Changelog
## [1.0.1](https://github.com/UiO-Gaming/uiogaming.no/compare/uiogaming.no-1.0.0...uiogaming.no-1.0.1) (2026-11-02)
### Bug Fixes
* filter function not working in firefox ([52e8087](https://github.com/UiO-Gaming/uiogaming.no/commit/52e8087229153d8465a9b257c1da5778b997e20a))
* photo credit text placement ([95f75d9](https://github.com/UiO-Gaming/uiogaming.no/commit/95f75d9bb1022407915da2eba5e9245e8b9e2ee4))
+11 -2
View File
@@ -1,7 +1,16 @@
# Huskeliste for bidrag
- Sørg for å formatere koden med Prettier
- Sørg for å formatere koden med Prettier (`npm run format`)
- Sjekk at koden ikke har lint-feil (`npm run lint`)
- Sjekk om nettsiden bygges uten problemer (`npm run build`)
- Formatering, linting, typesjekk og bygg kjøres også automatisk på alle pull requests
- 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
- Kode skal skrives på engelsk. Det vil si variabelnavn osv.
- Nettsiden finnes på både norsk og engelsk. Tekst som vises til bruker skal ikke skrives rett i komponentene, men legges inn i både [messages/no.json](messages/no.json) og [messages/en.json](messages/en.json)
+46
View File
@@ -0,0 +1,46 @@
FROM node:26-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:26-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_* values are inlined at build time, and pages are prerendered
# with data from Sanity during the build
ARG NEXT_PUBLIC_SANITY_PROJECT_ID
ARG NEXT_PUBLIC_SANITY_DATASET
ARG NEXT_PUBLIC_UMAMI_SRC
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
RUN npm run build
# Only the standalone server and its traced dependencies end up in the final
# image, not the full node_modules or the source
FROM node:26-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=9000
ENV HOSTNAME=0.0.0.0
# The server writes regenerated pages to .next, so the files must be owned by
# the user it runs as
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public
USER node
EXPOSE 9000
CMD ["node", "server.js"]
+39 -15
View File
@@ -1,38 +1,62 @@
# 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. Det kjører automatisk når du starter applikasjonen.
```bash
cd webhook-api
yarn install
yarn start
```
MEN
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.
Du må sette opp et webhook i [Sanity sitt dashbord](https://www.sanity.io/manage) under _API → Webhooks_:
### Docker
- **URL:** `https://uiogaming.no/api/revalidate`
- **HTTP method:** POST
- **Projection:** Må minst inneholde `_type` og `slug`, f.eks `{_type, slug}`
- **Secret:** Samme verdi som `SANITY_REVALIDATE_SECRET` i `.env`
Docker er for øyeblikket ikke støttet, men vil støttes fremtiden.
Sanity bruker _Secret_ til å signere hver forespørsel med headeren `sanity-webhook-signature`, og APIet avviser forespørsler uten gyldig signatur med `401`. Hemmeligheten skal altså ligge i _Secret_-feltet, ikke i URLen.
Webhooket oppdaterer blogginnlegg, arrangementer og forfattere med en gang. Annet innhold oppdateres først når cachen går ut (opptil en time).
Du kan lese mer om webhooks [her](https://www.sanity.io/docs/webhooks).
## Vil du bidra?
+17
View File
@@ -0,0 +1,17 @@
services:
uiogaming.no:
container_name: uiogaming.no
build:
context: .
# Values are read from .env
args:
NEXT_PUBLIC_SANITY_PROJECT_ID: ${NEXT_PUBLIC_SANITY_PROJECT_ID}
NEXT_PUBLIC_SANITY_DATASET: ${NEXT_PUBLIC_SANITY_DATASET}
NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID}
env_file: .env
ports:
- 9000:9000
environment:
- TZ=Europe/Oslo
restart: unless-stopped
+7
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
import nextTs from "eslint-config-next/typescript"
// eslint-config-next already ignores .next, out, build and next-env.d.ts
const eslintConfig = defineConfig([...nextVitals, ...nextTs])
export default eslintConfig
-50
View File
@@ -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`,
],
}
-53
View File
@@ -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,
},
})
})
}
Vendored
+4
View File
@@ -0,0 +1,4 @@
declare module "*.module.css" {
const classes: { readonly [key: string]: string }
export = classes
}
+58
View File
@@ -0,0 +1,58 @@
{
"common": {
"photo": "Photo",
"join": "Become a member",
"statutes": "Statutes",
"complaint": "Complaint",
"documents": "Documents"
},
"metaTags": {
"time": "Time",
"location": "Location",
"description": "A student organisation that aims to create an inclusive environment around gaming and e-sports for the students at UiO"
},
"buttons": {
"back": "back"
},
"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 👀",
"pcRoomImageAlt": "PC-room with 6 gaming setups"
},
"boardMembers": {
"title": "The Board",
"imageAlt": "Portrait of board member",
"noContact": "No contact info"
},
"contact": {
"title": "Find us",
"email": "E-mail"
},
"footer": {
"sourceCode": "Source code",
"admin": "Admin page",
"orgNum": "Org. No."
}
}
+58
View File
@@ -0,0 +1,58 @@
{
"common": {
"photo": "Foto",
"join": "Bli medlem",
"statutes": "Vedtekter",
"complaint": "Klage",
"documents": "Dokumenter"
},
"metaTags": {
"time": "Tid",
"location": "Sted",
"description": "En hobbyforening som ønsker å skape et sosialt miljø rundt gaming og e-sport på UiO!"
},
"buttons": {
"back": "tilbake"
},
"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 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 👀",
"pcRoomImageAlt": "PC-rom som inneholder 6 spillmaskiner"
},
"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."
}
}
+22
View File
@@ -0,0 +1,22 @@
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
const withNextIntl = createNextIntlPlugin()
const nextConfig: NextConfig = {
// Bundles a minimal server with only the needed dependencies, used by the
// Docker image
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.sanity.io",
port: "",
pathname: "/images/**",
},
],
},
}
export default withNextIntl(nextConfig)
+7034
View File
File diff suppressed because it is too large. Load diff
+27 -28
View File
@@ -1,38 +1,37 @@
{
"name": "uiogaming.no",
"version": "1.0.1",
"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": "eslint .",
"format": "prettier --write ."
},
"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": "^8.0.1",
"@sanity/client": "^8.14.1",
"@sanity/image-url": "^2.1.1",
"@sanity/webhook": "^4.0.4",
"country-flag-icons": "^1.6.20",
"next": "^16.4.3",
"next-intl": "^4.14.9",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-icons": "^5.7.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"
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"eslint": "^9.39.5",
"eslint-config-next": "^16.4.3",
"prettier": "^3.9.10",
"typescript": "~6.0.3"
},
"repository": {
"type": "git",
File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.7 MiB

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "node",
"include-v-in-tag": false
}
}
}
+6
View File
@@ -0,0 +1,6 @@
import { notFound } from "next/navigation"
// Catch unknown paths below a locale so they render [locale]/not-found.tsx
export default function CatchAll() {
notFound()
}
@@ -5,10 +5,12 @@
}
.backButton {
justify-self: start;
margin-left: 2rem;
}
.article {
display: grid;
place-items: center;
margin-bottom: 2rem;
}
.header {
display: grid;
@@ -21,6 +23,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 +58,7 @@
display: grid;
gap: 2em;
}
.content {
display: grid;
gap: 1rem;
@@ -60,11 +66,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 +78,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;
+185
View File
@@ -0,0 +1,185 @@
import Footer from "@/components/page-sections/footer"
import { generateSeoMetadata } from "@/components/seo"
import Back from "@/components/ui/back"
import { type SanityImage, urlFor } from "@/lib/imageUrlBuilder"
import { getBlogPost } from "@/lib/sanity"
import { PortableText, PortableTextComponents } from "@portabletext/react"
import { Metadata } from "next"
import type { Locale } from "next-intl"
import { notFound } from "next/navigation"
import Image from "next/image"
import * as styles from "./page.module.css"
interface BlogPageParams {
params: Promise<{
slug: string
locale: Locale
}>
}
type ImageValue = Partial<SanityImage> & {
alt?: string
caption?: string
}
interface LinkValue {
href: string
}
const SAFE_LINK_PROTOCOLS = ["http:", "https:", "mailto:"]
// Links come from the CMS, so reject schemes like javascript: that would run
// code when clicked. Parsing with URL also catches tricks like leading
// whitespace or mixed case that a simple prefix check would miss
const getSafeHref = (href?: string) => {
if (!href) return null
try {
const { protocol } = new URL(href, "https://uiogaming.no")
return SAFE_LINK_PROTOCOLS.includes(protocol) ? href : null
} catch {
return null
}
}
// Render posts on first visit and cache them, instead of on every request
export async function generateStaticParams() {
return []
}
export async function generateMetadata({
params,
}: BlogPageParams): Promise<Metadata> {
const { slug, locale } = await params
const data = await getBlogPost(slug)
if (!data) notFound()
return generateSeoMetadata({
locale,
path: `/blog/${slug}`,
title: data.title,
description: data.excerpt,
// Cropped around the hotspot to the size link previews expect
imageURL: data.mainImage?.asset
? urlFor(data.mainImage).width(1200).height(630).url()
: undefined,
author: data.author?.name,
publishedTime: data._createdAt,
})
}
const components: PortableTextComponents = {
types: {
image: ({ value }: { value: ImageValue }) => {
if (!value?.asset) return null
return (
<div className={styles.imageContainer}>
<Image
// Only a width, so the image keeps its shape and just the crop
// applies. fit("max") avoids upscaling small images
src={urlFor(value).width(1200).fit("max").url()}
alt={value.alt || "Blog image"}
width={800}
height={400}
sizes="(max-width: 640px) 100vw, 640px"
style={{ width: "100%", height: "auto" }}
/>
{value.caption && (
<p className={styles.imageCaption}>{value.caption}</p>
)}
</div>
)
},
},
marks: {
link: ({
children,
value,
}: {
children?: React.ReactNode
value?: LinkValue
}) => {
const href = getSafeHref(value?.href)
if (!href) return <>{children}</>
// Only links to other sites open in a new tab, not mailto: or links
// within this site
const isExternal = /^https?:\/\//i.test(href)
return (
<a
href={href}
className={styles.link}
{...(isExternal && { target: "_blank", rel: "noopener noreferrer" })}
>
{children}
</a>
)
},
},
}
const BlogPost = async ({ params }: BlogPageParams) => {
const { slug, locale } = await params
const data = await getBlogPost(slug)
if (!data) return notFound()
const createdAt = new Date(data._createdAt).toLocaleDateString(locale, {
day: "numeric",
month: "long",
year: "numeric",
})
return (
<>
<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).width(1600).fit("max").url()}
alt={data.title}
width={800}
height={400}
sizes="(max-width: 850px) 100vw, 850px"
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={data.author.name}
src={urlFor(data.author.image).width(100).height(100).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 />
</>
)
}
export default BlogPost
@@ -1,13 +1,9 @@
body {
display: grid;
place-content: center;
min-height: none;
}
.container {
display: grid;
place-items: center;
margin: 1rem;
place-content: center;
min-height: 100vh;
padding-block: 6rem;
padding-inline: 1rem;
}
.event {
@@ -29,8 +25,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;
}
+77
View File
@@ -0,0 +1,77 @@
import { generateSeoMetadata } from "@/components/seo"
import Back from "@/components/ui/back"
import { formatEventDate } from "@/lib/dates"
import { getEvent } from "@/lib/sanity"
import { Metadata } from "next"
import type { Locale } from "next-intl"
import { notFound } from "next/navigation"
import { getTranslations } from "next-intl/server"
import { FaCalendarAlt, FaMapMarkerAlt } from "react-icons/fa"
import * as styles from "./page.module.css"
interface EventPageParams {
params: Promise<{
slug: string
locale: Locale
}>
}
// Render events on first visit and cache them, instead of on every request
export async function generateStaticParams() {
return []
}
export async function generateMetadata({
params,
}: EventPageParams): Promise<Metadata> {
const { slug, locale } = await params
const data = await getEvent(slug)
if (!data) notFound()
const t = await getTranslations({ locale, namespace: "metaTags" })
return generateSeoMetadata({
locale,
path: `/event/${slug}`,
title: data.title,
description: [
`${t("time")}: ${formatEventDate(data.date, locale)}`,
data.location && `${t("location")}: ${data.location}`,
data.description && `\n${data.description}`,
]
.filter(Boolean)
.join("\n"),
})
}
const Event = async ({ params }: EventPageParams) => {
const { slug, locale } = await params
const data = await getEvent(slug)
if (!data) return notFound()
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>{formatEventDate(data.date, locale)}</p>
</div>
<p className={styles.description}>{data.description}</p>
</div>
</div>
)
}
export default Event
+68
View File
@@ -0,0 +1,68 @@
import "@/app/globals.css"
import { hasLocale, NextIntlClientProvider } from "next-intl"
import type { Metadata, Viewport } from "next"
import { getMessages } from "next-intl/server"
import { notFound } from "next/navigation"
import LanguageSelector from "@/components/ui/languageSelector"
import { SITE_URL, THEME_COLOR } from "@/components/seo"
import UmamiAnalytics from "@/components/umamiAnalytics"
import { routing } from "@/i18n/routing"
// Namespaces used by client components. Server components read their
// translations on the server, so only these need to be sent to the browser.
// Add to this list when a client component starts using a new namespace
const CLIENT_NAMESPACES = ["about", "buttons", "common", "office"]
export const metadata: Metadata = {
// Resolves relative URLs in metadata, like Open Graph images and alternates
metadataBase: new URL(SITE_URL),
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 const viewport: Viewport = {
themeColor: THEME_COLOR,
colorScheme: "dark",
}
export function generateStaticParams() {
return routing.locales.map(locale => ({ locale }))
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const { locale } = await params
// [locale] matches any first path segment, e.g. /favicon.txt or /de
if (!hasLocale(routing.locales, locale)) notFound()
const messages = await getMessages()
const clientMessages = Object.fromEntries(
CLIENT_NAMESPACES.map(namespace => [namespace, messages[namespace]])
)
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={clientMessages}>
<LanguageSelector />
<main>{children}</main>
<UmamiAnalytics />
</NextIntlClientProvider>
</body>
</html>
)
}
+9
View File
@@ -0,0 +1,9 @@
import NotFound from "@/components/page-sections/notFound"
import { getLocale } from "next-intl/server"
export default async function LocaleNotFound() {
const locale = await getLocale()
return <NotFound locale={locale} />
}
+41
View File
@@ -0,0 +1,41 @@
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 } from "@/components/seo"
import { Metadata } from "next"
import type { Locale } from "next-intl"
export const revalidate = 3600 // 1 hour
interface HomePageParams {
params: Promise<{
locale: Locale
}>
}
export async function generateMetadata({
params,
}: HomePageParams): Promise<Metadata> {
const { locale } = await params
return generateSeoMetadata({ locale, path: "/" })
}
export default function Home() {
return (
<>
<Hero />
<About />
<Events />
<Office />
<BoardMembers />
<Contact />
<Footer />
</>
)
}
+73
View File
@@ -0,0 +1,73 @@
import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook"
import { revalidateTag } from "next/cache"
import { NextRequest, NextResponse } from "next/server"
// Expire immediately so the first visit after a publish gets fresh content.
// The "max" profile would serve the stale version once more while refetching
const expireTag = (tag: string) => revalidateTag(tag, { expire: 0 })
export async function POST(request: NextRequest) {
const secret = process.env.SANITY_REVALIDATE_SECRET
if (!secret) {
console.error("SANITY_REVALIDATE_SECRET is not set")
return NextResponse.json(
{ message: "Webhook is not configured" },
{ status: 500 }
)
}
// Sanity signs the raw body with the webhook secret, so verify it before
// parsing
const rawBody = await request.text()
const signature = request.headers.get(SIGNATURE_HEADER_NAME) ?? ""
// Throws on a malformed signature header instead of returning false
const isValid = await isValidSignature(rawBody, signature, secret).catch(
() => false
)
if (!isValid) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 })
}
let body
try {
body = JSON.parse(rawBody)
} catch {
return NextResponse.json({ message: "Invalid JSON body" }, { status: 400 })
}
try {
const { _type, slug } = body
console.log(
`Revalidating content: ${_type}${slug ? ` (${slug.current})` : ""}`
)
if (_type === "post") {
expireTag("blog-posts")
if (slug?.current) {
expireTag(`post-${slug.current}`)
}
} else if (_type === "event") {
expireTag("events")
if (slug?.current) {
expireTag(`event-${slug.current}`)
}
} else if (_type === "author") {
expireTag("authors")
// Blog posts show the author's name and image
expireTag("blog-posts")
}
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 })
}
}
+21 -14
View File
@@ -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;
}
+14
View File
@@ -0,0 +1,14 @@
import { SITE_URL } from "@/components/seo"
import type { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: "/api/",
},
sitemap: `${SITE_URL}/sitemap.xml`,
}
}
+39
View File
@@ -0,0 +1,39 @@
import { getLocalizedPath, SITE_URL } from "@/components/seo"
import { routing } from "@/i18n/routing"
import { getSitemapDocuments } from "@/lib/sanity"
import type { MetadataRoute } from "next"
export const revalidate = 3600 // 1 hour
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const { posts, events } = await getSitemapDocuments()
const pages: { path: string; lastModified?: string }[] = [
{ path: "/" },
...posts.map(post => ({
path: `/blog/${post.slug}`,
lastModified: post._updatedAt,
})),
...events.map(event => ({
path: `/event/${event.slug}`,
lastModified: event._updatedAt,
})),
]
// One entry per language version, each linking to the other versions
return pages.flatMap(({ path, lastModified }) => {
const languages = Object.fromEntries(
routing.locales.map(locale => [
locale,
`${SITE_URL}${getLocalizedPath(locale, path)}`,
])
)
return routing.locales.map(locale => ({
url: languages[locale],
lastModified,
alternates: { languages },
}))
})
}
-16
View File
@@ -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
-14
View File
@@ -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);
}
-16
View File
@@ -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
@@ -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%;
}
}
+47
View File
@@ -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 id="about">
<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,75 @@
import { type SanityImage, urlFor } from "@/lib/imageUrlBuilder"
import { getBoardMembers } 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
contactInfo?: string
name: string
role?: {
no: string
en: string
}
image: SanityImage | null
bio?: {
no: string
en: string
}
}
const BoardMembers = async () => {
const t = await getTranslations("boardMembers")
const locale = await getLocale()
const data: BoardMember[] = await getBoardMembers()
return (
<section id="board-members">
<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
? // Cropped to a square around the hotspot
urlFor(member.image).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
+102
View File
@@ -0,0 +1,102 @@
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 id="contact">
<h2 className="section-center-header">{t("title")}</h2>
<div className={styles.container}>
<div className={styles.item}>
<FaAt />
<a href="mailto:styret@uiogaming.no">{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 {
+105
View File
@@ -0,0 +1,105 @@
import { formatEventDate } from "@/lib/dates"
import { getUpcomingEvents } 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 | null
}
const DESCRIPTION_PREVIEW_LENGTH = 70
const Events = async () => {
const t = await getTranslations("events")
const tCommon = await getTranslations("common")
const locale = await getLocale()
const events: Event[] = await getUpcomingEvents(2)
const previewDescription = (description: string | null) => {
if (!description) return null
// Count graphemes so emoji and accented characters are never cut in half
const graphemes = Array.from(new Intl.Segmenter().segment(description))
if (graphemes.length <= DESCRIPTION_PREVIEW_LENGTH) return description
const preview = graphemes
.slice(0, DESCRIPTION_PREVIEW_LENGTH)
.map(({ segment }) => segment)
.join("")
.trimEnd()
return (
<>
{preview}... <span className={styles.readMore}>{t("readMore")}</span>
</>
)
}
const mapEvents = (events: Event[]) => {
if (events.length !== 0) {
return events.map((event: Event) => (
<Link key={event._id} href={`/${locale}/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>{formatEventDate(event.date, locale)}</p>
</div>
</div>
<p>{previewDescription(event.description)}</p>
</article>
</Link>
))
} else {
return (
<h3 className={styles.noEvents}>
{t("noEvents")}
<br />
<br />
(っ˘̩╭╮˘̩)っ
</h3>
)
}
}
return (
<section id="events">
<div className={styles.container}>
<h2 className={styles.sectionHeader}>{t("title")}</h2>
<div className={styles.events}>{mapEvents(events)}</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;
+68
View File
@@ -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;
}
+24
View File
@@ -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
@@ -3,6 +3,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 1rem;
}
+29
View File
@@ -0,0 +1,29 @@
import type { Locale } from "next-intl"
import { getTranslations } from "next-intl/server"
import Link from "next/link"
import * as styles from "./notFound.module.css"
const NotFound = async ({ locale }: { locale: Locale }) => {
const t = await getTranslations({ locale, namespace: "404" })
return (
<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={`/${locale}`}
className={`${styles.centeredText} ${styles.homeLink}`}
>
{t("back")}
</Link>
</div>
)
}
export default NotFound
@@ -0,0 +1,39 @@
.container {
display: grid;
gap: 2rem;
place-items: center;
}
.panoramaContainer {
width: 100%;
aspect-ratio: 4 / 3;
max-height: 75vh;
}
.panoramaContainer > div {
width: 100% !important;
height: 100% !important;
}
.panoramaFallback {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.pcRoomImage {
width: 100%;
height: auto;
max-width: 60ch;
border-radius: 8px;
object-fit: contain;
}
.map {
width: 100%;
aspect-ratio: 4 / 3;
min-height: 400px;
max-height: 75vh;
border-radius: 8px;
}
+77
View File
@@ -0,0 +1,77 @@
"use client"
import { useSyncExternalStore } from "react"
import { useTranslations } from "next-intl"
import Image from "next/image"
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
import * as styles from "./office.module.css"
let webGL2Support: boolean | undefined
// Checked once, since creating a WebGL context is not free
const getWebGL2Support = () => {
webGL2Support ??= Boolean(
document.createElement("canvas").getContext("webgl2")
)
return webGL2Support
}
// WebGL support never changes, so there is nothing to subscribe to
const subscribe = () => () => {}
const Office = () => {
const t = useTranslations("office")
// null on the server, which renders the fallback image
const supportsWebGL2 = useSyncExternalStore(
subscribe,
getWebGL2Support,
() => null
)
return (
<section id="office">
<h2>{t("title")}</h2>
<div className={styles.container}>
<p>{t("text")}</p>
<p>{t("tip")}</p>
<div className={styles.panoramaContainer}>
{supportsWebGL2 === true ? (
<ReactPhotoSphereViewer
width={"100%"}
height={"100%"}
src="/3D_office_2025_08_20.jpg"
defaultZoomLvl={0}
navbar={true}
/>
) : (
<Image
src="/3D_office_2025_08_20.jpg"
alt={t("title")}
className={styles.panoramaFallback}
width={1200}
height={900}
/>
)}
</div>
<Image
src="/pc_room.jpg"
alt={t("pcRoomImageAlt")}
className={styles.pcRoomImage}
width={666}
height={500}
/>
<iframe
width={"100%"}
className={styles.map}
src="https://use.mazemap.com/embed.html#v=1&config=uio&zlevel=3&center=10.712463,59.932424&zoom=17&campusid=797&sharepoitype=poi&sharepoi=1000986035&utm_medium=iframe"
allow="geolocation"
loading="lazy"
title="Map by MazeMap"
></iframe>
</div>
</section>
)
}
export default Office
-61
View File
@@ -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
-65
View File
@@ -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
-76
View File
@@ -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
-101
View File
@@ -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
-68
View File
@@ -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
-38
View File
@@ -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
-23
View File
@@ -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;
}
-30
View File
@@ -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
-11
View File
@@ -1,11 +0,0 @@
.container {
display: grid;
gap: 2rem;
justify-content: center;
place-items: center;
}
.image {
width: 100%;
aspect-ratio: 1 / 1;
}
-124
View File
@@ -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
+95
View File
@@ -0,0 +1,95 @@
import { Metadata } from "next"
import type { Locale } from "next-intl"
import { getTranslations } from "next-intl/server"
import { routing } from "@/i18n/routing"
export const SITE_URL = "https://uiogaming.no"
export const THEME_COLOR = "#7d36e7"
/** Adds the locale prefix to a path, e.g. ("en", "/event/x") -> "/en/event/x" */
export const getLocalizedPath = (locale: Locale, path: string) =>
path === "/" ? `/${locale}` : `/${locale}${path}`
type SeoProps = {
locale: Locale
/** Path without the locale prefix, e.g. "/" or "/event/some-slug" */
path: string
title?: string
description?: string
imageURL?: string
author?: string
/** Marks the page as an article, e.g. a blog post */
publishedTime?: string
}
export async function generateSeoMetadata({
locale,
path,
title,
description,
imageURL,
author,
publishedTime,
}: SeoProps): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: "metaTags" })
const defaultTitle = "UiO Gaming"
const metaDescription = description || t("description")
return {
title: title ? `${title} | ${defaultTitle}` : defaultTitle,
description: metaDescription,
authors: author ? [{ name: author }] : [{ name: "UiO Gaming" }],
// Tells search engines that the Norwegian and English versions are the
// same page. The unprefixed path redirects based on the browser language
alternates: {
canonical: getLocalizedPath(locale, path),
languages: {
...Object.fromEntries(
routing.locales.map(pageLocale => [
pageLocale,
getLocalizedPath(pageLocale, path),
])
),
"x-default": path,
},
},
openGraph: {
title: title || defaultTitle,
description: metaDescription,
...(publishedTime
? { type: "article", publishedTime, authors: author && [author] }
: { type: "website" }),
locale: locale,
url: getLocalizedPath(locale, path),
siteName: defaultTitle,
images: imageURL
? [
{
url: imageURL,
width: 1200,
height: 630,
alt: title || defaultTitle,
},
]
: [
{
url: "/logo_2024_neon_trans.png",
alt: defaultTitle,
},
],
},
twitter: {
card: imageURL ? "summary_large_image" : "summary",
title: title || defaultTitle,
description: metaDescription,
images: imageURL ? [imageURL] : ["/logo_2024_neon_trans.png"],
},
other: {
"msapplication-TileColor": THEME_COLOR,
"msapplication-navbutton-color": THEME_COLOR,
"apple-mobile-web-app-status-bar-style": "black-translucent",
},
}
}
@@ -9,6 +9,7 @@
background: none;
border: none;
color: var(--link-color);
cursor: pointer;
}
.button:hover,
.button *:hover {
+44
View File
@@ -0,0 +1,44 @@
"use client"
import { useTranslations } from "next-intl"
import { FaArrowLeft } from "react-icons/fa"
import { useRouter } from "@/i18n/navigation"
import * as styles from "./back.module.css"
// document.referrer only reflects the last full page load, so it misses
// client-side navigations within the site. Only used where the Navigation
// API isn't supported
const cameFromThisSite = () =>
document.referrer !== "" &&
new URL(document.referrer).origin === window.location.origin
const Back = () => {
const router = useRouter()
const t = useTranslations("buttons")
const handleBack = () => {
// The Navigation API only lists this site's own history entries, so
// canGoBack is true exactly when the previous page was on this site
const canGoBack = window.navigation?.canGoBack ?? cameFromThisSite()
if (canGoBack) {
router.back()
} else {
router.push("/")
}
}
return (
<button
className={`${styles.container} ${styles.button}`}
onClick={handleBack}
>
<FaArrowLeft />
<span>{t("back")}</span>
</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,30 @@
.languageSwitcher {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.35);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.button:hover {
background: rgba(0, 0, 0, 1);
}
.flag {
width: 20px;
height: 15px;
border-radius: 2px;
}
+40
View File
@@ -0,0 +1,40 @@
"use client"
import { GB, NO } from "country-flag-icons/react/3x2"
import { type Locale, useLocale } from "next-intl"
import { usePathname, useRouter } from "@/i18n/navigation"
import * as styles from "./languageSelector.module.css"
const LanguageSelector = () => {
const locale = useLocale()
const router = useRouter()
// Pathname without the locale prefix
const pathname = usePathname()
const changeLocale = (newLocale: Locale) => {
// Read at click time, since useSearchParams() would opt pages out of
// static rendering
const { search, hash } = window.location
router.push(`${pathname}${search}${hash}`, { locale: newLocale })
}
return (
<div className={styles.languageSwitcher}>
{locale === "no" ? (
<button onClick={() => changeLocale("en")} className={styles.button}>
<GB className={styles.flag} aria-hidden="true" />
<span lang="en">English</span>
</button>
) : (
<button onClick={() => changeLocale("no")} className={styles.button}>
<NO className={styles.flag} aria-hidden="true" />
<span lang="no">Norsk</span>
</button>
)}
</div>
)
}
export default LanguageSelector
+20
View File
@@ -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;
}
+19
View File
@@ -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
+80
View File
@@ -0,0 +1,80 @@
.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;
}
.dropdownLink {
display: block;
width: 100%;
margin-top: 4px;
color: black;
background-color: #cccccc;
border-radius: 999em;
text-align: left;
font-size: 1.6rem;
box-shadow: var(--shadow);
padding: 0.5em 1em;
padding-right: 2em;
}
.dropdownLink:first-child {
margin-top: 8px;
}
.dropdownLink: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);
}
+103
View File
@@ -0,0 +1,103 @@
"use client"
import { useEffect, useId, useRef, 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(open => !open)
const containerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const dropdownId = useId()
// Close when clicking outside the dropdown or pressing Escape
useEffect(() => {
if (!isOpen) return
const handlePointerDown = (event: PointerEvent) => {
if (!containerRef.current?.contains(event.target as Node)) {
setIsOpen(false)
}
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsOpen(false)
buttonRef.current?.focus()
}
}
document.addEventListener("pointerdown", handlePointerDown)
document.addEventListener("keydown", handleKeyDown)
return () => {
document.removeEventListener("pointerdown", handlePointerDown)
document.removeEventListener("keydown", handleKeyDown)
}
}, [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
ref={containerRef}
className={styles.dropdownContainer}
// Close when keyboard focus moves out of the dropdown
onBlur={event => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setIsOpen(false)
}
}}
>
<button
ref={buttonRef}
onClick={toggleDropdown}
className={styles.button}
aria-expanded={isOpen}
aria-controls={dropdownId}
>
{t("other")}{" "}
<span
className={isOpen ? styles.arrowUp : styles.arrowDown}
aria-hidden="true"
></span>
</button>
{isOpen && (
<div id={dropdownId} className={styles.dropdown}>
{links.map(link => (
<a
key={link.url}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={styles.dropdownLink}
>
{link.label}
</a>
))}
</div>
)}
</div>
)
}
export default OtherButton
+22
View File
@@ -0,0 +1,22 @@
import Script from "next/script"
// Umami ignores visits from other domains, so local and preview deployments
// aren't counted
const DOMAINS = ["uiogaming.no", "www.uiogaming.no"]
export default function UmamiAnalytics() {
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 || process.env.NODE_ENV !== "production") return null
return (
<Script
src={src}
data-website-id={websiteId}
data-domains={DOMAINS.join(",")}
strategy="afterInteractive"
/>
)
}
+7
View File
@@ -0,0 +1,7 @@
import { createNavigation } from "next-intl/navigation"
import { routing } from "./routing"
// Locale-aware wrappers around Next's navigation APIs
export const { Link, redirect, usePathname, useRouter, getPathname } =
createNavigation(routing)
+20
View File
@@ -0,0 +1,20 @@
import { hasLocale } from "next-intl"
import { getRequestConfig } from "next-intl/server"
import { locale as rootLocale } from "next/root-params"
import { routing } from "./routing"
export default getRequestConfig(async ({ locale }) => {
// An explicit locale (e.g. getTranslations({ locale })) takes precedence.
// The root param can be any first path segment until the layout rejects it,
// so fall back to the default instead of throwing
const requested = locale ?? (await rootLocale())
const resolved = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale
return {
locale: resolved,
messages: (await import(`../../messages/${resolved}.json`)).default,
}
})
+14
View File
@@ -0,0 +1,14 @@
import { defineRouting } from "next-intl/routing"
export const routing = defineRouting({
locales: ["no", "en"],
defaultLocale: "no",
localePrefix: "always",
})
// Types getLocale(), useLocale() etc. as "no" | "en" instead of string
declare module "next-intl" {
interface AppConfig {
Locale: (typeof routing.locales)[number]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

-10
View File
@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more