Skip to content

Commit 447329c

Browse files
committed
feat: migrate site to Next.js
1 parent fedb963 commit 447329c

31 files changed

Lines changed: 8327 additions & 1267 deletions

.github/workflows/deploy-pages.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Deploy GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: pages
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Enable Corepack
26+
run: corepack enable
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 22
32+
cache: pnpm
33+
34+
- name: Install dependencies
35+
run: pnpm install --frozen-lockfile
36+
37+
- name: Build static site
38+
run: pnpm build
39+
40+
- name: Setup Pages
41+
uses: actions/configure-pages@v5
42+
43+
- name: Upload Pages artifact
44+
uses: actions/upload-pages-artifact@v3
45+
with:
46+
path: out
47+
48+
deploy:
49+
needs: build
50+
runs-on: ubuntu-latest
51+
environment:
52+
name: github-pages
53+
url: ${{ steps.deployment.outputs.page_url }}
54+
steps:
55+
- name: Deploy to GitHub Pages
56+
id: deployment
57+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
.superpowers/
2+
.next/
3+
node_modules/
4+
out/

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
11
# rssnext.github.io
2+
3+
Static RSS3 site built with Next.js.
4+
5+
## Development
6+
7+
```bash
8+
pnpm install
9+
pnpm dev
10+
```
11+
12+
## Static export
13+
14+
```bash
15+
pnpm build
16+
```
17+
18+
The build writes a fully static site to `out/`, including `CNAME`, `.nojekyll`,
19+
the Merkle JSON, and the static blog HTML files under `public/`.
20+
21+
## Migration portal config
22+
23+
The migration page uses RainbowKit for wallet connection and wagmi/viem for contract
24+
reads and writes. Update `public/airdrop.config.json` before deployment:
25+
26+
```json
27+
{
28+
"contractAddress": "0x0000000000000000000000000000000000000000",
29+
"requiredChainId": "0x1",
30+
"merkleUrl": "/assets/final_airdrop_merkle.json"
31+
}
32+
```
33+
34+
Set `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` during build to enable WalletConnect
35+
inside RainbowKit. Without it, the static build still uses RainbowKit, but only
36+
browser-injected wallets are shown.

airdrop.config.js

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
export default function HomeInteractions() {
6+
useEffect(() => {
7+
const root = document.documentElement;
8+
const sectionLinks = [...document.querySelectorAll("[data-section-link]")];
9+
const revealTargets = [...document.querySelectorAll("[data-reveal]")];
10+
const pageSections = [...document.querySelectorAll("main section[id]")];
11+
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
12+
const cleanups = [];
13+
14+
const setReducedMotion = (matches) => {
15+
root.classList.toggle("reduced-motion", matches);
16+
};
17+
18+
setReducedMotion(reducedMotionQuery.matches);
19+
20+
const onReducedMotionChange = (event) => setReducedMotion(event.matches);
21+
if (typeof reducedMotionQuery.addEventListener === "function") {
22+
reducedMotionQuery.addEventListener("change", onReducedMotionChange);
23+
cleanups.push(() => reducedMotionQuery.removeEventListener("change", onReducedMotionChange));
24+
}
25+
26+
if (!reducedMotionQuery.matches) {
27+
const revealObserver = new IntersectionObserver(
28+
(entries) => {
29+
entries.forEach((entry) => {
30+
if (entry.isIntersecting) {
31+
entry.target.classList.add("is-visible");
32+
revealObserver.unobserve(entry.target);
33+
}
34+
});
35+
},
36+
{
37+
threshold: 0.2,
38+
rootMargin: "0px 0px -8% 0px",
39+
}
40+
);
41+
42+
revealTargets.forEach((target) => revealObserver.observe(target));
43+
cleanups.push(() => revealObserver.disconnect());
44+
} else {
45+
revealTargets.forEach((target) => target.classList.add("is-visible"));
46+
}
47+
48+
const sectionObserver = new IntersectionObserver(
49+
(entries) => {
50+
const visibleEntry = entries
51+
.filter((entry) => entry.isIntersecting)
52+
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
53+
54+
if (!visibleEntry) {
55+
return;
56+
}
57+
58+
const currentId = visibleEntry.target.id;
59+
sectionLinks.forEach((link) => {
60+
link.classList.toggle("is-active", link.dataset.sectionLink === currentId);
61+
});
62+
},
63+
{
64+
threshold: [0.3, 0.5, 0.7],
65+
rootMargin: "-20% 0px -45% 0px",
66+
}
67+
);
68+
69+
pageSections.forEach((section) => sectionObserver.observe(section));
70+
cleanups.push(() => sectionObserver.disconnect());
71+
72+
const syncScrollProgress = () => {
73+
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
74+
const progress = scrollable > 0 ? window.scrollY / scrollable : 0;
75+
root.style.setProperty("--scroll", progress.toFixed(4));
76+
};
77+
78+
syncScrollProgress();
79+
window.addEventListener("scroll", syncScrollProgress, { passive: true });
80+
window.addEventListener("resize", syncScrollProgress);
81+
cleanups.push(() => {
82+
window.removeEventListener("scroll", syncScrollProgress);
83+
window.removeEventListener("resize", syncScrollProgress);
84+
});
85+
86+
if (!reducedMotionQuery.matches) {
87+
const onPointerMove = (event) => {
88+
const x = `${((event.clientX / window.innerWidth) * 100).toFixed(2)}%`;
89+
const y = `${((event.clientY / window.innerHeight) * 100).toFixed(2)}%`;
90+
root.style.setProperty("--pointer-x", x);
91+
root.style.setProperty("--pointer-y", y);
92+
};
93+
94+
window.addEventListener("pointermove", onPointerMove, { passive: true });
95+
cleanups.push(() => window.removeEventListener("pointermove", onPointerMove));
96+
}
97+
98+
return () => cleanups.forEach((cleanup) => cleanup());
99+
}, []);
100+
101+
return null;
102+
}

app/components/SiteFooter.jsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export default function SiteFooter() {
2+
return (
3+
<footer className="site-footer">
4+
<div className="wrap footer-inner">
5+
<div className="footer-copy">
6+
<p>RSS3</p>
7+
<p>Open information, rebuilt with care.</p>
8+
</div>
9+
<div className="footer-socials" aria-label="Social media">
10+
<a
11+
className="social-link"
12+
href="https://github.com/RSSNext"
13+
target="_blank"
14+
rel="noreferrer"
15+
aria-label="RSS3 on GitHub"
16+
>
17+
<span className="social-link__icon" aria-hidden="true">
18+
<svg viewBox="0 0 24 24" focusable="false">
19+
<path
20+
fill="currentColor"
21+
d="M12 1.5a10.5 10.5 0 0 0-3.32 20.46c.53.1.72-.22.72-.5v-1.94c-2.93.64-3.55-1.24-3.55-1.24-.48-1.2-1.16-1.52-1.16-1.52-.95-.65.07-.64.07-.64 1.05.08 1.6 1.08 1.6 1.08.93 1.6 2.45 1.14 3.05.87.09-.67.36-1.14.66-1.41-2.34-.27-4.81-1.17-4.81-5.2 0-1.15.41-2.1 1.08-2.84-.11-.27-.47-1.37.1-2.86 0 0 .88-.28 2.89 1.08a10.02 10.02 0 0 1 5.26 0c2.01-1.36 2.89-1.08 2.89-1.08.57 1.49.21 2.59.1 2.86.67.74 1.08 1.69 1.08 2.84 0 4.04-2.48 4.92-4.84 5.19.37.32.7.96.7 1.94v2.88c0 .28.19.61.73.5A10.5 10.5 0 0 0 12 1.5Z"
22+
/>
23+
</svg>
24+
</span>
25+
<span>GitHub</span>
26+
</a>
27+
<a
28+
className="social-link"
29+
href="https://x.com/rss3_"
30+
target="_blank"
31+
rel="noreferrer"
32+
aria-label="RSS3 on X"
33+
>
34+
<span className="social-link__icon" aria-hidden="true">
35+
<svg viewBox="0 0 24 24" focusable="false">
36+
<path
37+
fill="currentColor"
38+
d="M18.9 2.25H22l-6.78 7.74L23.2 21.75h-6.24l-4.9-6.92-6.05 6.92H2.9l7.25-8.29L.8 2.25h6.4l4.43 6.27 5.48-6.27Zm-1.1 17.62h1.72L6.25 4.03H4.4l13.4 15.84Z"
39+
/>
40+
</svg>
41+
</span>
42+
<span>Twitter</span>
43+
</a>
44+
<a className="social-link" href="mailto:contact@rss3.io" aria-label="Email RSS3">
45+
<span className="social-link__icon" aria-hidden="true">
46+
<svg viewBox="0 0 24 24" focusable="false">
47+
<path
48+
fill="currentColor"
49+
d="M3 5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25v13.5A2.25 2.25 0 0 1 18.75 21H5.25A2.25 2.25 0 0 1 3 18.75V5.25Zm2.06.39 6.43 5.36a.75.75 0 0 0 .96 0l6.43-5.36a.75.75 0 0 0-.48-.14H5.54a.75.75 0 0 0-.48.14Zm14.44 1.32-5.57 4.64a2.25 2.25 0 0 1-2.88 0L5.5 6.96v11.79c0 .41.34.75.75.75h12.5c.41 0 .75-.34.75-.75V6.96Z"
50+
/>
51+
</svg>
52+
</span>
53+
<span>contact@rss3.io</span>
54+
</a>
55+
</div>
56+
</div>
57+
</footer>
58+
);
59+
}

app/components/SiteHeader.jsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const navItems = [
2+
["Vision", "/#vision", "vision"],
3+
["Folo", "/#folo", "folo"],
4+
["RSSHub", "/#rsshub", "rsshub"],
5+
["$RSS3", "/#rss3", "rss3"],
6+
["Blog", "/#blog", "blog"],
7+
["Ecosystem", "/#ecosystem", "ecosystem"],
8+
];
9+
10+
export default function SiteHeader({ activeItem = null, withSectionTracking = false }) {
11+
return (
12+
<header className="site-header">
13+
<div className="wrap header-inner">
14+
<a className="brand" href="/#hero" aria-label="RSS3 home">
15+
<img className="brand-mark" src="/assets/logo.png" alt="" aria-hidden="true" />
16+
<span className="brand-text">RSS3</span>
17+
</a>
18+
19+
<nav className="site-nav" aria-label="Primary">
20+
{navItems.map(([label, href, key]) => (
21+
<a
22+
key={key}
23+
href={href}
24+
className={activeItem === key ? "is-active" : undefined}
25+
aria-current={activeItem === key ? "page" : undefined}
26+
data-section-link={withSectionTracking ? key : undefined}
27+
>
28+
{label}
29+
</a>
30+
))}
31+
</nav>
32+
</div>
33+
</header>
34+
);
35+
}

0 commit comments

Comments
 (0)