diff --git a/.eslintrc.js b/.eslintrc.js
index 3725308..0151529 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,37 +1,3 @@
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,
- },
+ extends: 'next/core-web-vitals',
};
diff --git a/.gitignore b/.gitignore
index e23ed7b..76ac9b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,17 @@
-*.log
+*.log*
+*.pem
+*.tsbuildinfo
.DS_Store
+.env*.local
.history
.next
+.pnp.js
+.vercel
+.yarn
+/.pnp
+/build
+/coverage
+next-env.d.ts
node_modules
out
-package-lock.json
\ No newline at end of file
+package-lock.json
diff --git a/.nvmrc b/.nvmrc
index b6a7d89..209e3ef 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-16
+20
diff --git a/LICENSE.md b/LICENSE.md
index 3a8ee75..2b6f091 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,7 @@
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..178d110 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
![Travis](https://travis-ci.org/koodiklinikka/koodiklinikka.fi.svg?branch=master)
-
+
**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.
@@ -20,8 +20,7 @@
### Vaaditut työkalut
-- Asenna [Node.js](http://nodejs.org)
-- Asenna [Yarn 1.x](https://classic.yarnpkg.com/en/)
+- Asenna [Node.js](http://nodejs.org) ja [Yarn 1.x](https://classic.yarnpkg.com/en/) (tai [Bun](https://bun.sh/))
- Asenna [Git](https://git-scm.com/) client lähdekoodin hallintaan
### Kloonaa projekti koneellesi
@@ -44,14 +43,14 @@ Avaa selaimessasi: [`http://localhost:3000`](http://localhost:3000)
## Komennot
-### `yarn`
+### `yarn` (tai `bun i`)
Asentaa projektin riippuvuudet
-### `yarn start`
+### `yarn dev` (tai `bun dev`)
-Kääntää lähdetiedostot ja palvelee sovellusta porttiin `3000`
+Palvelee sovellusta kehitystilassa porttiin `3000`
-### `yarn build`
+### `yarn build` (tai `bun run build`)
Kääntää lähdetiedostot -> `out/` -hakemistoon
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..a9901c3
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.jpg b/app/opengraph-image.jpg
new file mode 100644
index 0000000..94ab1b0
Binary files /dev/null and b/app/opengraph-image.jpg differ
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..ea62eeb
--- /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.jpg b/app/twitter-image.jpg
new file mode 100644
index 0000000..94ab1b0
Binary files /dev/null and b/app/twitter-image.jpg differ
diff --git a/assets/banner.png b/assets/banner.png
new file mode 100644
index 0000000..16318e2
Binary files /dev/null and b/assets/banner.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..c5c2901
--- /dev/null
+++ b/components/ChannelGrid.tsx
@@ -0,0 +1,33 @@
+import shuffle from 'lodash.shuffle';
+
+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]);
+
+export default function ChannelGrid({ channels }: { channels: Channel[] }) {
+ 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 (
-