diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index e1e62b8..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-# http://editorconfig.org
-root = true
-
-[*]
-indent_style = space
-indent_size = 2
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-[*.md]
-trim_trailing_whitespace = false
-
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index c585e19..0000000
--- a/.eslintignore
+++ /dev/null
@@ -1 +0,0 @@
-out
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 3725308..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,37 +0,0 @@
-module.exports = {
- extends: [
- "plugin:@typescript-eslint/recommended",
- "plugin:react-hooks/recommended",
- "plugin:react/recommended",
- "plugin:jsx-a11y/recommended",
- "plugin:@next/next/recommended",
- "prettier",
- ],
- settings: {
- react: {
- version: "detect",
- },
- },
- env: {
- browser: true,
- node: true,
- },
- parser: "@typescript-eslint/parser",
- parserOptions: {
- ecmaVersion: 2020,
- sourceType: "module",
- ecmaFeatures: {
- jsx: true,
- },
- },
- rules: {
- "@typescript-eslint/ban-types": "warn",
- "no-use-before-define": 0,
- "padded-blocks": 0,
- "react/jsx-no-target-blank": 0,
- "react/jsx-uses-react": 2,
- "react/jsx-uses-vars": 2,
- "react/prop-types": 0,
- "react/react-in-jsx-scope": 2,
- },
-};
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
deleted file mode 100644
index 97a3608..0000000
--- a/.github/workflows/main.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-name: Scheduled build
-on:
- schedule:
- - cron: "0 0 * * *"
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Trigger our build webhook on Netlify
- run: curl -s -X POST "https://api.netlify.com/build_hooks/${TOKEN}"
- env:
- TOKEN: ${{ secrets.NETLIFY_CRON_BUILD_HOOK }}
diff --git a/.gitignore b/.gitignore
index e23ed7b..fd3dbb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,36 @@
-*.log
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
.DS_Store
-.history
-.next
-node_modules
-out
-package-lock.json
\ No newline at end of file
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/.nvmrc b/.nvmrc
index b6a7d89..209e3ef 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-16
+20
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index 2b3533c..0000000
--- a/.prettierignore
+++ /dev/null
@@ -1,2 +0,0 @@
-.next
-out
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..d98169b
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,8 @@
+{
+ "printWidth": 120,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "useTabs": false,
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index be473d4..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-script: yarn build
-language: node_js
-node_js:
- - "12.18"
-install:
- - yarn
-test:
- - yarn lint
- - yarn build
-deploy:
- local_dir: out
- provider: pages
- fqdn: koodiklinikka.fi
- skip_cleanup: true
- github_token: "$GITHUB_TOKEN"
- repo: koodiklinikka/koodiklinikka.github.io
- target_branch: master
- on:
- branch: master
diff --git a/LICENSE.md b/LICENSE.md
index 3a8ee75..d713c8f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015 Riku Rouvila
+Copyright (c) 2024 Petri Partio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index ddf37f4..c403366 100644
--- a/README.md
+++ b/README.md
@@ -1,57 +1,36 @@
-# Koodiklinikka
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
+## Getting Started
-
+First, run the development server:
-**Koodiklinikka.fi lähdekoodi**. [Issueita](https://github.com/koodiklinikka/koodiklinikka.fi/issues) ja [Pull Requestejä](https://github.com/koodiklinikka/koodiklinikka.fi/pulls) otetaan lämpimästi vastaan. Yritämme pitää kynnyksen kontribuoida projektiin alhaisena, jotta mahdollisimman moni pääsisi jättämään siihen jälkensä. Kaikki koodi katselmoidaan läpi ja mergetään projektiin kun näyttää hyvälle. Muutamasta mergetystä Pull Requestista oikeudet ylläpitää projektia.
-
-[Issueita](https://github.com/koodiklinikka/koodiklinikka.fi/issues) voidaan käyttää myös sivun:
-
-- toiminnallisuuteen
-- designiin
-- [HTTP-rajapintaan](https://github.com/koodiklinikka/koodiklinikka.fi-api)
-- projektin hallintaan liittyviin asioihin
-- tai koko Koodiklinikkaan yleisesti.
-
----
-
-## Projektin asennus
-
-### Vaaditut työkalut
-
-- Asenna [Node.js](http://nodejs.org)
-- Asenna [Yarn 1.x](https://classic.yarnpkg.com/en/)
-- Asenna [Git](https://git-scm.com/) client lähdekoodin hallintaan
-
-### Kloonaa projekti koneellesi
-
-```sh
-git clone https://github.com/koodiklinikka/koodiklinikka.fi.git
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
```
-### Käynnistä kehitystila
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-```sh
-cd koodiklinikka.fi
-yarn
-yarn start
-```
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-### Avaa esikatselu selaimessa
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-Avaa selaimessasi: [`http://localhost:3000`](http://localhost:3000)
+## Learn More
-## Komennot
+To learn more about Next.js, take a look at the following resources:
-### `yarn`
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-Asentaa projektin riippuvuudet
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-### `yarn start`
+## Deploy on Vercel
-Kääntää lähdetiedostot ja palvelee sovellusta porttiin `3000`
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-### `yarn build`
-
-Kääntää lähdetiedostot -> `out/` -hakemistoon
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/TODO.md b/TODO.md
deleted file mode 100644
index b1c77d2..0000000
--- a/TODO.md
+++ /dev/null
@@ -1,6 +0,0 @@
-- Stripe
- - Test ID `pk_test_OmNve9H1OuORlmD4rblpjgzh`
- - Prod ID `pk_live_xrnwdLNXbt20LMxpIDffJnnC`
-- API integration (test backend `https://lit-plateau-4689.herokuapp.com/`)
-- Hero video
-- Deployment
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..d805cbf
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,99 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+
+ .text-shadow {
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ }
+}
+
+.title-highlight {
+ background: linear-gradient(200deg, #ff0098 20%, #0ef 80%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ filter: drop-shadow(0 0 20px rgba(255, 0, 234, 0.2));
+}
+
+@supports (color: color(display-p3 1 1 1)) {
+ .title-highlight {
+ background: linear-gradient(200deg, oklch(68% 0.5 340) 20%, oklch(90% 0.5 200) 100%);
+
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ filter: drop-shadow(0 0 20px oklch(80% 0.41 211 / 20%));
+ }
+}
+
+.bg-button {
+ background: linear-gradient(200deg, #f0f 20%, #ff00c4 100%);
+}
+
+@supports (color: color(display-p3 1 1 1)) {
+ .bg-button {
+ background: linear-gradient(200deg, oklch(100% 0.5 340) 20%, oklch(86% 0.5 360) 100%);
+ }
+}
+
+html {
+ background: #070b1e url('../public/background.webp');
+ background-size: 1024px auto;
+ background-position: top center;
+ background-repeat: no-repeat;
+ scroll-behavior: smooth;
+}
+
+@media (min-width: 1024px) {
+ html {
+ background-size: 100% auto;
+ }
+}
+
+h1,
+h2,
+h3 {
+ text-wrap: balance;
+}
+
+.checkbox svg {
+ display: none;
+}
+
+input[type='checkbox']:checked + .checkbox {
+ background-color: #ef008e;
+ border-color: #ff0099;
+}
+
+input[type='checkbox']:checked + .checkbox svg {
+ display: block;
+ align-items: center;
+ justify-items: center;
+ width: 80%;
+ height: auto;
+}
+
+input[type='checkbox']:focus + .checkbox {
+ outline: 2px solid var(--tw-color-red-800);
+ outline-offset: 2px;
+}
+
+@keyframes fadeInOut {
+ 0% {
+ opacity: 20%;
+ }
+ 50% {
+ opacity: 100%;
+ }
+ 100% {
+ opacity: 20%;
+ }
+}
+
+.fade-in-out {
+ opacity: 20%;
+ animation: fadeInOut 4s ease-in-out infinite;
+}
diff --git a/app/icon.png b/app/icon.png
new file mode 100644
index 0000000..c664406
Binary files /dev/null and b/app/icon.png differ
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..31b6bca
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,27 @@
+import type { Metadata } from 'next';
+import { Inter } from 'next/font/google';
+import './globals.css';
+import BottomFade from '@/components/BottomFade';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata: Metadata = {
+ title: 'Koodiklinikka',
+ description: 'Yhteisö kaikille ohjelmoinnista ja ohjelmistoalasta kiinnostuneille harrastajille ja ammattilaisille',
+ robots: 'noindex',
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/app/opengraph-image.alt.txt b/app/opengraph-image.alt.txt
new file mode 100644
index 0000000..37f6a87
--- /dev/null
+++ b/app/opengraph-image.alt.txt
@@ -0,0 +1 @@
+Koodiklinikka on Suomen suurin ohjelmistoalan yhteisö, joka tuo alan ammattilaiset ja harrastajat yhteen
diff --git a/app/opengraph-image.png b/app/opengraph-image.png
new file mode 100644
index 0000000..297ef3f
Binary files /dev/null and b/app/opengraph-image.png differ
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..57b2f91
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,105 @@
+import shuffle from 'lodash.shuffle';
+
+import ChannelGrid from '@/components/ChannelGrid';
+import FeatureImage from '@/components/FeatureImage';
+import Footer from '@/components/Footer';
+import Hero from '@/components/Hero';
+import Nav from '@/components/Nav';
+import Wrapper from '@/components/Wrapper';
+
+async function getChannels() {
+ const res = await fetch('https://stats.koodiklinikka.fi/api/channels', { next: { revalidate: 3600 } });
+
+ if (!res.ok) {
+ // This will activate the closest `error.js` Error Boundary
+ throw new Error('Failed to fetch data');
+ }
+
+ return res.json();
+}
+
+export default async function Home() {
+ let channels: Channel[] = await getChannels();
+ channels = channels.sort((a, b) => (a.messages_today > b.messages_today ? -1 : 1));
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Suosituimmat keskustelunaiheet tänään
+
+
+
+
+
+ Ja paljon muuta:{' '}
+ {shuffle(channels.splice(0, 20))
+ .map
((channel) => (
+
+ #{channel.name}
+
+ ))
+ .reduce((prev, curr) => [prev, ', ', curr])}
+ …
+
+
+
+
+
+
+
+
+
+
Yhteisö ohjelmoinnista kiinnostuneille
+
+
+ Koodiklinikka on Suomen suurin ohjelmistoalan yhteisö, joka kokoaa yhteen ammattilaiset, harrastajat
+ ja vasta-alkajat. Tavoitteenamme on yhdistää ja kasvattaa suomalaista ohjelmointiyhteisöä sekä
+ tarjota apua ja uusia kontakteja kaikille ohjelmoinnista innostuneille.
+
+
+
+ Liittyminen on ilmaista ja helppoa. Jätä sähköpostiosoitteesi{' '}
+
+ yllä olevaan kenttään
+
+ , niin lähetämme sinulle kutsun Slack-yhteisöömme.
+
+
+
+
+
+
+
+
+
+
+
Avoin lähdekoodi <3
+
+
+ Suosimme avointa lähdekoodia ja kaikki käyttämämme koodi on vapaasti saatavilla sekä
+ hyödynnettävissä Github-organisaatiomme sivulta. Organisaation jäseneksi otamme kaikki
+ Slack-yhteisömme jäsenet. Koodiklinikan projekteihin voi osallistua kuka tahansa ja muutosideat ovat
+ aina lämpimästi tervetulleita!
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/twitter-image.alt.txt b/app/twitter-image.alt.txt
new file mode 100644
index 0000000..37f6a87
--- /dev/null
+++ b/app/twitter-image.alt.txt
@@ -0,0 +1 @@
+Koodiklinikka on Suomen suurin ohjelmistoalan yhteisö, joka tuo alan ammattilaiset ja harrastajat yhteen
diff --git a/app/twitter-image.png b/app/twitter-image.png
new file mode 100644
index 0000000..297ef3f
Binary files /dev/null and b/app/twitter-image.png differ
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..0ca80a2
Binary files /dev/null and b/bun.lockb differ
diff --git a/components/BottomFade.tsx b/components/BottomFade.tsx
new file mode 100644
index 0000000..c57c839
--- /dev/null
+++ b/components/BottomFade.tsx
@@ -0,0 +1,5 @@
+export default function BottomFade() {
+ return (
+
+ );
+}
diff --git a/components/ChannelGrid.tsx b/components/ChannelGrid.tsx
new file mode 100644
index 0000000..9888285
--- /dev/null
+++ b/components/ChannelGrid.tsx
@@ -0,0 +1,33 @@
+import shuffle from 'lodash.shuffle';
+
+export default function ChannelGrid({ channels }: { channels: Channel[] }) {
+ const DELAYS = shuffle([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1]);
+
+ return (
+
+ {channels.map((channel, i) => (
+
+ ))}
+
+ );
+}
diff --git a/components/ChannelReferenceRenderer.tsx b/components/ChannelReferenceRenderer.tsx
deleted file mode 100644
index adf2127..0000000
--- a/components/ChannelReferenceRenderer.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/* eslint-disable @typescript-eslint/ban-types */
-import React from "react";
-
-function renderStringWithChannelRefs(value: string) {
- return (
- <>
- {value.split(/(<#[A-Z0-9]+\|[A-Za-z0-9]+>)/).map((str, i) => {
- const matches = str.match(/<#([A-Z0-9]+)\|([A-Za-z0-9]+)>/);
- if (matches) {
- return (
-
- #{matches[2]}
-
- );
- }
- return {str} ;
- })}
- >
- );
-}
-
-export const ChannelReferenceRenderer = ({
- children,
-}: React.PropsWithChildren<{}>) => {
- // TODO: this should probably walk the tree
- if (typeof children[0] === "string")
- return renderStringWithChannelRefs(children[0]);
- return <>{children}>;
-};
diff --git a/components/EmailComponent.tsx b/components/EmailComponent.tsx
deleted file mode 100644
index ab11279..0000000
--- a/components/EmailComponent.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from "react";
-
-export default function EmailComponent() {
- return info@koodiklinikka.fi ;
-}
diff --git a/components/Fader.tsx b/components/Fader.tsx
deleted file mode 100644
index b0cb508..0000000
--- a/components/Fader.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from "react";
-
-type Props = {
- threshold: number;
-};
-
-function clamp(min, max, value) {
- return Math.min(Math.max(value, min), max);
-}
-
-export default class Fader extends React.Component {
- static defaultProps = {
- threshold: 100,
- };
-
- state = {
- opacity: 0,
- };
-
- onScroll = () => {
- const scrollableDistance = document.body.scrollHeight - window.innerHeight,
- scrollTop = window.pageYOffset || document.documentElement.scrollTop,
- distanceToBottom = scrollableDistance - scrollTop;
-
- this.setState({
- opacity: clamp(0, 1, distanceToBottom / this.props.threshold),
- });
- };
-
- componentDidMount() {
- window.addEventListener("scroll", this.onScroll);
- this.onScroll();
- }
-
- componentWillUnmount() {
- window.removeEventListener("scroll", this.onScroll);
- }
-
- render() {
- const style = {
- opacity: this.state.opacity,
- };
-
- return
;
- }
-}
diff --git a/components/FeatureImage.tsx b/components/FeatureImage.tsx
new file mode 100644
index 0000000..426608c
--- /dev/null
+++ b/components/FeatureImage.tsx
@@ -0,0 +1,27 @@
+import Image from 'next/image';
+
+export default function FeatureImage({
+ src,
+ width,
+ height,
+ alt,
+}: {
+ src: string;
+ width: number | `${number}`;
+ height: number | `${number}`;
+ alt: string;
+}) {
+ return (
+
+ );
+}
diff --git a/components/Feed.tsx b/components/Feed.tsx
deleted file mode 100644
index bd0a5bc..0000000
--- a/components/Feed.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import flatMap from "lodash/flatMap";
-import sortBy from "lodash/sortBy";
-import React from "react";
-import request from "axios";
-import api from "./api";
-import transformers from "./feed-transformers";
-import ReactTimeAgo from "react-time-ago";
-import JavascriptTimeAgo from "javascript-time-ago";
-import timeagoFi from "javascript-time-ago/locale/fi";
-
-JavascriptTimeAgo.addLocale(timeagoFi);
-
-export default class Feed extends React.Component {
- state = {
- messages: [],
- };
-
- componentDidMount() {
- this.updateFeed();
- }
-
- async updateFeed() {
- const res = await request.get(api("feeds"));
- const messages = sortBy(
- flatMap(res.data, (messages, type) => transformers[type](messages)),
- "timestamp"
- );
- messages.reverse(); // In-place
- this.setState({
- messages: messages.slice(0, 40),
- });
- }
-
- render() {
- const messages = this.state.messages.map((message, i) => {
- let image = ;
-
- if (message.imageLink) {
- image = (
-
- {image}
-
- );
- }
-
- return (
-
-
- {image}
-
-
-
-
-
-
-
-
-
-
-
- {message.meta}
-
-
-
- );
- });
-
- return {messages}
;
- }
-}
diff --git a/components/Footer.tsx b/components/Footer.tsx
index d278a8d..26e5758 100644
--- a/components/Footer.tsx
+++ b/components/Footer.tsx
@@ -1,76 +1,49 @@
-import React from "react";
-import EmailComponent from "./EmailComponent";
-import sponsors from "../data/sponsors";
+import Image from 'next/image';
-type Props = {
- href: string;
- name: string;
- title?: string;
-};
-
-const SponsorLink = ({ href, name }: Props) => (
-
-
-
-);
-
-const SocialLink = ({ href, name, title }: Props) => (
-
-
-
-);
-
-export function Footer() {
+export default function Footer() {
return (
-