Compare commits

...
5 Commits
Author SHA1 Message Date
niklas fe129bc92e docs: update README from default nextjs one
Deploy / deploy (push) Successful in 44s
2026-09-28 22:11:48 +02:00
niklas 9e734f29b8 chore: remove packageManager field from package.json
Deploy / deploy (push) Successful in 45s
2026-09-28 22:04:42 +02:00
niklas b8f7168d64 chore: build a workflow to deploy site automatically
Deploy / deploy (push) Failing after 4s
2026-09-28 22:03:55 +02:00
niklas d8eb5d7504 update landing page with grassy cloud fields 2026-09-28 21:57:12 +02:00
niklas 61a87b1f9a feat: migrate to pnpm and nextjs16 2026-09-28 21:57:04 +02:00
16 changed files with 5936 additions and 3113 deletions
+34
View File
@@ -0,0 +1,34 @@
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 12
- uses: actions/setup-node@v4
with:
node-version: 26
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm exec wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+7
View File
@@ -39,3 +39,10 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
#Ignore vscode AI rules
.github/instructions/codacy.instructions.md
# wrangler
.wrangler
+37 -29
View File
@@ -1,36 +1,44 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). # Woiken Website
## Getting Started The landing page for [woiken.com](https://woiken.com), a coming-soon page for Woiken.
First, run the development server: It's a single page built with Next.js 16 and exported as static files. Scrolling opens a
framed sky photo up to full screen, where the "A new cloud, coming soon." tagline fades in.
```bash ## Commands
npm run dev
# or | Command | Action |
yarn dev | :------------------------- | :-------------------------------------------------- |
# or | `pnpm install` | Install dependencies |
pnpm dev | `pnpm dev` | Start the dev server at `localhost:3000` |
# or | `pnpm build` | Build the static site to `./out/` |
bun dev | `pnpm lint` | Lint with ESLint |
| `pnpm exec wrangler dev` | Serve `./out/` locally the way Cloudflare will |
Use pnpm 12. `pnpm start` doesn't work, because the site is a static export (`output: "export"`).
## Project structure
```
.
├── app/
│ ├── page.tsx # The landing page
│ ├── layout.tsx
│ └── globals.css # Tailwind + photo darkening
├── components/
│ └── ScrollExpand/ # Scroll-to-expand hero (from React Bits)
├── public/
│ └── grass-field.jpg # Hero photo
├── wrangler.jsonc # Cloudflare Worker config
└── .gitea/workflows/deploy.yml
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ## Deployment
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. Every push to `main` deploys the site via `.gitea/workflows/deploy.yml`. The workflow builds
the site and runs `wrangler deploy`, which uploads `./out/` as static assets to the
`woiken-website` Worker on the `woiken.com` custom domain.
## Credits
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. Hero photo: [grass field at daytime](https://unsplash.com/photos/grass-field-at-daytime-PEm_sLmJT-w)
on Unsplash.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [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.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
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.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+5
View File
@@ -2,3 +2,8 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Tone down the bright sky so the white type reads on top of it */
.woiken-hero .scroll-expand__media {
filter: brightness(0.72) saturate(1.1);
}
+23 -18
View File
@@ -1,26 +1,31 @@
"use client"; import ScrollExpand from "@/components/ScrollExpand/ScrollExpand";
import { motion } from "framer-motion";
export default function Home() { export default function Home() {
return ( return (
<div className="min-h-screen bg-[#0b0b08] text-white"> <div className="min-h-screen bg-[#0b0b08] text-white">
<header className="mx-auto flex w-full max-w-6xl items-center justify-center px-6 py-6 md:px-10"> <ScrollExpand
<div className="text-lg font-semibold tracking-wide">Woiken</div> className="woiken-hero"
</header> src="/grass-field.jpg"
alt="Clouds drifting over a green field under a blue sky"
<motion.main title="Woiken"
initial={{ opacity: 0, y: 18 }} scrollHint="Scroll"
animate={{ opacity: 1, y: 0 }} startWidth={46}
transition={{ duration: 0.55, ease: "easeOut" }} startHeight={56}
className="mx-auto flex min-h-[calc(100vh-84px)] w-full max-w-6xl flex-col items-center justify-center px-6 pb-20 text-center md:px-10" overlayScrim={0.55}
useWindowScroll
> >
<section className="max-w-3xl"> <p className="mb-4 text-xs font-medium uppercase tracking-[0.3em] text-white/70 md:text-sm">
<h1 className="text-4xl font-medium leading-tight tracking-tight md:text-6xl"> Woiken
Woiken is coming soon. </p>
</h1> <h1 className="text-5xl font-semibold leading-[1.05] tracking-tight md:text-7xl lg:text-8xl">
</section> A new cloud
</motion.main> </h1>
<p className="mt-5 text-lg text-white/80 md:text-2xl">coming soon.</p>
</ScrollExpand>
<footer className="px-6 py-10 text-center text-sm text-white/40">
© {new Date().getFullYear()} Woiken
</footer>
</div> </div>
); );
} }
+102
View File
@@ -0,0 +1,102 @@
.scroll-expand {
position: relative;
width: 100%;
height: 100%;
}
.scroll-expand--scroller {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
overscroll-behavior: contain;
}
.scroll-expand--scroller::-webkit-scrollbar {
display: none;
}
.scroll-expand__track {
position: relative;
width: 100%;
}
.scroll-expand__stage {
position: sticky;
top: 0;
width: 100%;
overflow: hidden;
--se-title-size: 4rem;
}
.scroll-expand__frame {
position: absolute;
inset: 0;
clip-path: inset(21% 29% 21% 29% round 24px);
will-change: clip-path;
}
.scroll-expand__media {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
will-change: transform;
transform-origin: center;
user-select: none;
-webkit-user-drag: none;
}
.scroll-expand__scrim {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.1) 45%, rgba(0, 0, 0, 0.35));
opacity: 0;
}
.scroll-expand__overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6%;
opacity: 0;
will-change: opacity, transform;
}
.scroll-expand__title {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0 6%;
text-align: center;
font-size: var(--se-title-size);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1;
color: #fff;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);
pointer-events: none;
will-change: opacity, transform;
}
.scroll-expand__hint {
position: absolute;
left: 0;
right: 0;
bottom: 1.25rem;
text-align: center;
font-size: 0.8125rem;
letter-spacing: 0.02em;
color: rgba(255, 255, 255, 0.55);
pointer-events: none;
will-change: opacity, transform;
}
+28
View File
@@ -0,0 +1,28 @@
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
export interface ScrollExpandProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
src?: string;
mediaType?: "image" | "video";
poster?: string;
alt?: string;
title?: string;
scrollHint?: string;
startWidth?: number;
startHeight?: number;
startRadius?: number;
endRadius?: number;
mediaZoom?: number;
scrollDistance?: number;
holdDistance?: number;
smoothing?: number;
overlayScrim?: number;
useWindowScroll?: boolean;
enabled?: boolean;
children?: ReactNode;
className?: string;
style?: CSSProperties;
}
declare const ScrollExpand: (props: ScrollExpandProps) => JSX.Element;
export default ScrollExpand;
+243
View File
@@ -0,0 +1,243 @@
'use client';
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import './ScrollExpand.css';
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const smoothstep = (edge0, edge1, x) => {
const t = clamp((x - edge0) / (edge1 - edge0 || 1e-6), 0, 1);
return t * t * (3 - 2 * t);
};
const ScrollExpand = ({
src = '',
mediaType = 'image',
poster = '',
alt = '',
title = '',
scrollHint = '',
startWidth = 42,
startHeight = 58,
startRadius = 24,
endRadius = 0,
mediaZoom = 1.35,
scrollDistance = 1.2,
holdDistance = 0.35,
smoothing = 0.1,
overlayScrim = 0.45,
useWindowScroll = false,
enabled = true,
children,
className = '',
style,
...rest
}) => {
const rootRef = useRef(null);
const trackRef = useRef(null);
const stageRef = useRef(null);
const frameRef = useRef(null);
const mediaRef = useRef(null);
const titleRef = useRef(null);
const overlayRef = useRef(null);
const scrimRef = useRef(null);
const hintRef = useRef(null);
const propsRef = useRef({});
useLayoutEffect(() => {
propsRef.current = {
startWidth,
startHeight,
startRadius,
endRadius,
mediaZoom,
scrollDistance,
holdDistance,
smoothing,
overlayScrim,
useWindowScroll,
enabled
};
});
const applyProgress = useCallback(p => {
const frame = frameRef.current;
const media = mediaRef.current;
if (!frame || !media) return;
const c = propsRef.current;
const e = smoothstep(0, 1, p);
const w = c.startWidth + (100 - c.startWidth) * e;
const h = c.startHeight + (100 - c.startHeight) * e;
const ix = Math.max(0, (100 - w) / 2);
const iy = Math.max(0, (100 - h) / 2);
const r = c.startRadius + (c.endRadius - c.startRadius) * e;
frame.style.clipPath = `inset(${iy}% ${ix}% ${iy}% ${ix}% round ${r}px)`;
media.style.transform = `scale(${c.mediaZoom + (1 - c.mediaZoom) * e})`;
if (scrimRef.current) scrimRef.current.style.opacity = `${c.overlayScrim * e}`;
if (titleRef.current) {
const out = smoothstep(0.4, 0.88, p);
titleRef.current.style.opacity = `${1 - out}`;
titleRef.current.style.transform = `translate3d(0, ${-28 * out}px, 0) scale(${1 + 0.06 * out})`;
}
if (hintRef.current) {
const gone = smoothstep(0, 0.12, p);
hintRef.current.style.opacity = `${1 - gone}`;
hintRef.current.style.transform = `translate3d(0, ${8 * gone}px, 0)`;
}
if (overlayRef.current) {
const inn = smoothstep(0.68, 1, p);
overlayRef.current.style.opacity = `${inn}`;
overlayRef.current.style.transform = `translate3d(0, ${18 * (1 - inn)}px, 0)`;
}
}, []);
useEffect(() => {
const root = rootRef.current;
const track = trackRef.current;
const stage = stageRef.current;
if (!root || !track || !stage) return;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let raf = 0;
let current = 0;
let target = 0;
let stageH = 0;
let running = false;
const measure = () => {
const c = propsRef.current;
stageH = c.useWindowScroll ? window.innerHeight : root.clientHeight;
if (stageH <= 0) return;
stage.style.height = `${stageH}px`;
track.style.height = `${stageH * (1 + Math.max(0, c.scrollDistance) + Math.max(0, c.holdDistance))}px`;
const w = root.clientWidth || stageH;
stage.style.setProperty('--se-title-size', `${clamp(w * 0.075, 20, 84)}px`);
};
const readProgress = () => {
const c = propsRef.current;
if (!c.enabled) return 1;
const span = stageH * Math.max(0.01, c.scrollDistance);
if (c.useWindowScroll) {
const top = track.getBoundingClientRect().top;
return clamp(-top / span, 0, 1);
}
return clamp(root.scrollTop / span, 0, 1);
};
const tick = () => {
const c = propsRef.current;
const k = c.smoothing <= 0 ? 1 : 1 - Math.exp(-1 / (60 * c.smoothing));
current += (target - current) * k;
if (Math.abs(target - current) < 0.0004) {
current = target;
running = false;
}
applyProgress(current);
raf = running ? requestAnimationFrame(tick) : 0;
};
const kick = () => {
if (running) return;
running = true;
if (!raf) raf = requestAnimationFrame(tick);
};
const onScroll = () => {
target = readProgress();
if (propsRef.current.smoothing <= 0 || reduceMotion) {
current = target;
applyProgress(current);
return;
}
kick();
};
const onResize = () => {
measure();
target = readProgress();
current = target;
applyProgress(current);
};
measure();
target = readProgress();
current = target;
applyProgress(current);
const scroller = useWindowScroll ? window : root;
scroller.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onResize);
const ro = new ResizeObserver(onResize);
ro.observe(root);
return () => {
if (raf) cancelAnimationFrame(raf);
scroller.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
ro.disconnect();
};
}, [applyProgress, useWindowScroll]);
const media =
mediaType === 'video' ? (
<video
ref={mediaRef}
className="scroll-expand__media"
src={src}
poster={poster}
autoPlay
muted
loop
playsInline
/>
) : (
// eslint-disable-next-line @next/next/no-img-element
<img ref={mediaRef} className="scroll-expand__media" src={src} alt={alt} draggable={false} />
);
return (
<div
ref={rootRef}
className={`scroll-expand ${useWindowScroll ? '' : 'scroll-expand--scroller'} ${className}`.trim()}
style={style}
{...rest}
>
<div ref={trackRef} className="scroll-expand__track">
<div ref={stageRef} className="scroll-expand__stage">
<div ref={frameRef} className="scroll-expand__frame">
{media}
<div ref={scrimRef} className="scroll-expand__scrim" />
{children ? (
<div ref={overlayRef} className="scroll-expand__overlay">
{children}
</div>
) : null}
</div>
{title ? (
<div ref={titleRef} className="scroll-expand__title">
{title}
</div>
) : null}
{scrollHint ? (
<div ref={hintRef} className="scroll-expand__hint">
{scrollHint}
</div>
) : null}
</div>
</div>
</div>
);
};
export default ScrollExpand;
+8 -13
View File
@@ -1,16 +1,11 @@
import { dirname } from "path"; import { defineConfig, globalIgnores } from "eslint/config";
import { fileURLToPath } from "url"; import nextVitals from "eslint-config-next/core-web-vitals";
import { FlatCompat } from "@eslint/eslintrc"; import nextTs from "eslint-config-next/typescript";
const __filename = fileURLToPath(import.meta.url); const eslintConfig = defineConfig([
const __dirname = dirname(__filename); ...nextVitals,
...nextTs,
const compat = new FlatCompat({ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "tailwind.config.ts"]),
baseDirectory: __dirname, ]);
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig; export default eslintConfig;
+12 -13
View File
@@ -3,33 +3,32 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"motion": "^12.4.7", "motion": "^12.4.7",
"next": "15.1.7", "next": "16.3.6",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "^19.0.0", "react": "^19.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.3.0",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19.3.0",
"@types/react-dom": "^19", "@types/react-dom": "^19.3.0",
"eslint": "^9", "eslint": "^9.39.5",
"eslint-config-next": "15.1.7", "eslint-config-next": "16.3.6",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5",
}, "wrangler": "^4.142.0"
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" }
} }
+5404
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
esbuild: true
sharp: false
workerd: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

+19 -5
View File
@@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -19,9 +23,19 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "woiken-website",
"compatibility_date": "2026-09-28",
"assets": {
"directory": "./out",
"not_found_handling": "404-page"
},
"routes": [{ "pattern": "woiken.com", "custom_domain": true }]
}
-3035
View File
File diff suppressed because it is too large Load Diff