Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ static/moonc-worker.js

# Generated files
static/rabbita-home/
static/rabbita-2026-scc-showcase/
src/static/rabbita-home/
.docusaurus
.cache-loader
Expand Down
111 changes: 111 additions & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export default async (): Promise<Config> => {
} else if (
entry.name.endsWith('.mbt') ||
entry.name.endsWith('.mbti') ||
entry.name === 'moon.pkg' ||
entry.name === 'moon.pkg.json' ||
entry.name === 'moon.mod.json'
) {
Expand Down Expand Up @@ -246,6 +247,116 @@ export default async (): Promise<Config> => {
},
}
},
// In dev mode, watch showcase .mbt source files and run moon build on
// each change, then copy output to static/rabbita-2026-scc-showcase/
// for Docusaurus to serve.
function rabbitaShowcasePlugin() {
const showcaseDir = path.join(
process.cwd(),
'src',
'rabbita',
'2026-scc-showcase'
)
const staticDir = path.join(
process.cwd(),
'static',
'rabbita-2026-scc-showcase'
)
const stylesSrc = path.join(showcaseDir, 'styles.css')

function collectWatchFiles(dir: string, files: string[] = []): string[] {
const skip = new Set(['node_modules', '_build', 'target', '.git'])
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (skip.has(entry.name)) continue
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
collectWatchFiles(full, files)
} else if (
entry.name.endsWith('.mbt') ||
entry.name.endsWith('.mbti') ||
entry.name === 'moon.pkg' ||
entry.name === 'moon.pkg.json' ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Watch the actual Moon package file name

The showcase watch list includes moon.pkg.json, but this project uses moon.pkg (src/rabbita/2026-scc-showcase/main/moon.pkg). As a result, editing package metadata/options in dev mode will not trigger buildAndCopy(), so /rabbita-2026-scc-showcase/main.js can stay stale until the Docusaurus process is restarted.

Useful? React with 👍 / 👎.

entry.name === 'moon.mod.json'
) {
files.push(full)
}
}
return files
}

function findFirstJs(dir: string): string | null {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
const found = findFirstJs(full)
if (found) return found
} else if (entry.name.endsWith('.js')) {
return full
}
}
return null
}

function buildAndCopy(): void {
const result = spawnSync('moon', ['build', '--target', 'js', '--debug'], {
cwd: showcaseDir,
stdio: 'inherit',
})
if (result.status !== 0) return
const buildDir = path.join(showcaseDir, '_build', 'js', 'debug', 'build')
if (!existsSync(buildDir)) return
const jsFile = findFirstJs(buildDir)
if (!jsFile) return
if (!existsSync(staticDir)) mkdirSync(staticDir, { recursive: true })
const code = readFileSync(jsFile, 'utf8')
.replace(/\n?\/\/[#@]\s*sourceMappingURL=.*$/m, '')
.replace(/\n?\/\*#\s*sourceMappingURL=.*?\*\//m, '')
writeFileSync(path.join(staticDir, 'main.js'), code)
if (existsSync(stylesSrc)) {
copyFileSync(stylesSrc, path.join(staticDir, 'styles.css'))
}
}

return {
name: 'rabbita-showcase',
getPathsToWatch() {
const files = collectWatchFiles(showcaseDir)
if (existsSync(stylesSrc)) files.push(stylesSrc)
return files
},
async loadContent() {
if (process.env.NODE_ENV === 'development') {
buildAndCopy()
}
return null
},
injectHtmlTags() {
if (process.env.NODE_ENV !== 'development') return {}
return {
headTags: [
{
tagName: 'script',
innerHTML: `
(function () {
var known = null;
setInterval(async function () {
if (!document.getElementById('rabbita-scc-showcase')) return;
try {
var r = await fetch('/rabbita-2026-scc-showcase/main.js', { method: 'HEAD', cache: 'no-store' });
if (!r.ok) return;
var tag = r.headers.get('etag') || r.headers.get('last-modified');
if (known === null) { known = tag; return; }
if (tag !== known) { known = tag; location.reload(); }
} catch (e) {}
}, 500);
})();
`,
},
],
}
},
}
},
[
'./plugins/blog-plugin',
{
Expand Down
8 changes: 7 additions & 1 deletion i18n/zh/docusaurus-plugin-content-pages/2026-scc/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ContestLayout from '@site/src/components/ContestLayout'
import ContestNavbar, { items2026 } from '@site/src/components/ContestNavbar'
import styles from '@site/src/pages/2026-scc/styles.module.css'

export default function Page() {
Expand All @@ -7,9 +8,11 @@ export default function Page() {
heroImg='/img/2026-contest/kv.jpg'
mobileHeroImg='/img/2026-contest/kv.jpg'
qqGroupImg='/img/2026-contest/qq-group.png'
heroBgColor='#051033'
heroBgColor='#09184c'
heroBackdropImg='/img/2026-contest/kv.jpg'
>
<div className={styles.container}>
<ContestNavbar activeIndex={0} items={items2026} qqGroup={false} />
<ChineseContent />
</div>
</ContestLayout>
Expand Down Expand Up @@ -48,6 +51,9 @@ function ChineseContent() {
>
立即报名
</a>
<a href='/2026-scc/showcase/' className='button button--primary button--lg'>
查看作品
</a>
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ContestLayout from '@site/src/components/ContestLayout'
import ContestNavbar, { items2026 } from '@site/src/components/ContestNavbar'
import RabbitaShowcaseMount from '@site/src/pages/2026-scc/showcase/RabbitaShowcaseMount'
import styles from '@site/src/pages/2026-scc/showcase/wrapper.module.css'

export default function Page() {
return (
<ContestLayout
heroImg='/img/2026-contest/kv.jpg'
mobileHeroImg='/img/2026-contest/kv.jpg'
qqGroupImg='/img/2026-contest/qq-group.png'
heroBgColor='#09184c'
heroBackdropImg='/img/2026-contest/kv.jpg'
>
<div className={styles.shell}>
<ContestNavbar activeIndex={1} items={items2026} qqGroup={false} />
<RabbitaShowcaseMount
lightModeLabel='白天模式'
darkModeLabel='夜间模式'
toggleLabel='展示墙主题切换'
/>
</div>
</ContestLayout>
)
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --host 0.0.0.0",
"dev": "pnpm rabbita-home:build && (cd src/pages/rabbita-home && npm run watch &) && docusaurus start --host 0.0.0.0",
"dev": "pnpm rabbita-home:build && pnpm rabbita-showcase:build && (cd src/pages/rabbita-home && npm run watch &) && docusaurus start --host 0.0.0.0",
"data": "tsx ./scripts/gen-data.ts",
"build": "pnpm rabbita-home:build && docusaurus build",
"build": "pnpm rabbita-home:build && pnpm rabbita-showcase:build && docusaurus build",
"rabbita-home:build": "node ./scripts/build-rabbita-home.mjs",
"rabbita-showcase:build": "node ./scripts/build-rabbita-showcase.mjs",
"postbuild": "cp -R static-gallery/* build/gallery/",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
Expand Down
56 changes: 56 additions & 0 deletions scripts/build-rabbita-showcase.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { spawnSync } from 'node:child_process'
import { cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { readdirSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'

const websiteDir = process.cwd()
const showcaseDir = path.resolve(websiteDir, 'src/rabbita/2026-scc-showcase')
const targetDir = path.resolve(websiteDir, 'static/rabbita-2026-scc-showcase')
const stylesSrc = path.resolve(showcaseDir, 'styles.css')

function findFirstJs(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
const nested = findFirstJs(full)
if (nested) return nested
} else if (entry.name.endsWith('.js')) {
return full
}
}
return null
}

const build = spawnSync('moon', ['build', '--target', 'js', '--release'], {
cwd: showcaseDir,
stdio: 'inherit',
})

if (build.status !== 0) {
process.exit(build.status ?? 1)
}

const buildDir = path.resolve(showcaseDir, '_build/js/release/build')
const jsFile = findFirstJs(buildDir)

if (!jsFile) {
console.error('[rabbita-showcase] failed to locate compiled main.js output')
process.exit(1)
}

const code = await stat(jsFile).then(() =>
readFile(jsFile, 'utf8')
).then((text) =>
text
.replace(/\n?\/\/[#@]\s*sourceMappingURL=.*$/m, '')
.replace(/\n?\/\*#\s*sourceMappingURL=.*?\*\//m, '')
)

await stat(stylesSrc)
await rm(targetDir, { recursive: true, force: true })
await mkdir(targetDir, { recursive: true })
await writeFile(path.join(targetDir, 'main.js'), code)
await cp(stylesSrc, path.join(targetDir, 'styles.css'))

console.log(`[rabbita-showcase] synced ${showcaseDir} -> ${targetDir}`)
29 changes: 25 additions & 4 deletions src/components/ContestLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Layout from '@theme/Layout'
import styles from './styles.module.css'
import type { CSSProperties } from 'react'

type HeroProps = {
img: string
mobileHeroImg: string
bgColor?: string
backdropImg?: string
}

type LayoutProps = {
Expand All @@ -13,14 +15,28 @@ type LayoutProps = {
qqGroupImg?: string
qqGroupId?: string
heroBgColor?: string
heroBackdropImg?: string
children: React.ReactNode
}

export default function ContestLayout(props: LayoutProps) {
const { heroImg, mobileHeroImg, children, qqGroupId, qqGroupImg, heroBgColor } = props
const {
heroImg,
mobileHeroImg,
children,
qqGroupId,
qqGroupImg,
heroBgColor,
heroBackdropImg
} = props
return (
<Layout>
<Hero img={heroImg} mobileHeroImg={mobileHeroImg} bgColor={heroBgColor} />
<Hero
img={heroImg}
mobileHeroImg={mobileHeroImg}
bgColor={heroBgColor}
backdropImg={heroBackdropImg}
/>
<main>
{qqGroupImg && (
<div className={styles['qq-group']}>
Expand All @@ -41,9 +57,14 @@ export default function ContestLayout(props: LayoutProps) {
)
}

function Hero({ img, mobileHeroImg, bgColor }: HeroProps) {
function Hero({ img, mobileHeroImg, bgColor, backdropImg }: HeroProps) {
const style: CSSProperties & Record<'--hero-bg-image' | '--hero-bg-color', string> = {
'--hero-bg-color': bgColor || 'black',
'--hero-bg-image': backdropImg ? `url(${backdropImg})` : 'none'
}

return (
<div className={styles['hero']} style={bgColor ? { backgroundColor: bgColor } : undefined}>
<div className={styles['hero']} style={style}>
<img className={styles['hero-img']} src={img} alt='' />
<img className={styles['mobile-hero-img']} src={mobileHeroImg} alt='' />
</div>
Expand Down
35 changes: 34 additions & 1 deletion src/components/ContestLayout/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
.hero {
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-color: black;
overflow: hidden;
isolation: isolate;
background-color: var(--hero-bg-color, black);
}

.hero::before {
content: '';
position: absolute;
inset: 0;
background-image: var(--hero-bg-image, none);
background-position: center;
background-size: cover;
background-repeat: no-repeat;
filter: blur(22px) saturate(0.9);
transform: scale(1.08);
opacity: 0.88;
pointer-events: none;
}

.hero::after {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(4, 8, 18, 0.08), rgba(4, 8, 18, 0.16));
pointer-events: none;
}

.hero img {
position: relative;
z-index: 1;
width: 100%;
max-width: 1200px;
}
Expand All @@ -19,6 +47,11 @@
}

@media (max-width: 996px) {
.hero::before {
filter: blur(14px) saturate(0.92);
transform: scale(1.04);
}

.mobile-hero-img {
display: block;
}
Expand Down
8 changes: 7 additions & 1 deletion src/components/ContestNavbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,13 @@ export const items2025en: ContestNavbarItem[] = [
]

export const items2026: ContestNavbarItem[] = [
{ name: '赛事章程', href: '/2026-scc' }
{ name: '赛事章程', href: '/2026-scc' },
{ name: '作品展示墙', href: '/2026-scc/showcase' }
]

export const items2026en: ContestNavbarItem[] = [
{ name: 'Regulations', href: '/2026-scc' },
{ name: 'Showcase Wall', href: '/2026-scc/showcase' }
]

export default function ContestNavbar({
Expand Down
Loading