diff --git a/.github/workflows/website-docker-pr.yml b/.github/workflows/website-docker-pr.yml deleted file mode 100644 index 6a97e9f..0000000 --- a/.github/workflows/website-docker-pr.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Test Build Website Docker Image (PR) - -on: - pull_request: - branches: - - master - paths: - - 'apps/website/**' - - '.github/workflows/website-docker.yml' - - '.github/workflows/website-docker-pr.yml' - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/website - -jobs: - test-build: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Build Docker image - uses: docker/build-push-action@v7 - with: - context: . - file: ./apps/website/Dockerfile - push: false - secrets: | - "github_token=${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/website-docker.yml b/.github/workflows/website-docker.yml deleted file mode 100644 index 9b15fa0..0000000 --- a/.github/workflows/website-docker.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Build and Push Website Docker Image - -on: - push: - branches: - - master - paths: - - 'apps/website/**' - - '.github/workflows/website-docker.yml' - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/website - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v4 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v6 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=sha,prefix={{branch}}- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Docker image - uses: docker/build-push-action@v7 - with: - context: . - file: ./apps/website/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - secrets: | - "github_token=${{ secrets.GITHUB_TOKEN }}" - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Trigger Deployment - run: | - curl -X 'POST' \ - '${{ secrets.DOKPLOY_DOMAIN }}/api/application.deploy' \ - -H 'accept: application/json' \ - -H 'x-api-key: ${{ secrets.DOKPLOY_API_KEY }}' \ - -H 'Content-Type: application/json' \ - -d '{ - "applicationId": "${{ secrets.WEBSITE_APPLICATION_ID }}" - }' diff --git a/.gitignore b/.gitignore index d600c7a..d491acf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,9 @@ node_modules/ .env.production.local .env.local -# Next.js files -.next -out -next-env.d.ts +# Vite and React Router output +.react-router/ +build/ # Fumadocs files .source diff --git a/README.md b/README.md index 648e397..432ca41 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ $ bun atlas:build # Build Atlas only ``` > [!NOTE] -> Atlas hosted on Cloudflare using R2, Queue, Hyperdrive, and Rate Limits +> Atlas hosted on Cloudflare using R2, Queue, Hyperdrive, and Rate Limits
> Website is built using a Dockerfile and runs as an image on our own infrastructure ## Contributing diff --git a/apps/website/Dockerfile b/apps/website/Dockerfile deleted file mode 100644 index 8894063..0000000 --- a/apps/website/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Build stage -FROM oven/bun:1 AS builder - -WORKDIR /app - -# Copy root package.json and workspace configuration -COPY package.json bun.lock ./ -COPY apps/website/package.json ./apps/website/ - -# Install dependencies -RUN bun install --filter @bx-team/website --ignore-scripts - -# Copy application source -COPY apps/website ./apps/website - -# Run postinstall scripts -WORKDIR /app/apps/website -RUN bun run postinstall - -# Build the application -RUN --mount=type=secret,id=github_token \ - GITHUB_TOKEN=$(cat /run/secrets/github_token) bun run build - -# Production stage -FROM oven/bun:1-slim - -WORKDIR /app - -ENV NODE_ENV=production -ENV PORT=3000 - -# Copy standalone build -COPY --from=builder /app/apps/website/.next/standalone ./ -COPY --from=builder /app/apps/website/.next/static ./apps/website/.next/static -COPY --from=builder /app/apps/website/public ./apps/website/public - -EXPOSE 3000 - -# Start the application -CMD ["bun", "apps/website/server.js"] diff --git a/apps/website/app/app.css b/apps/website/app/app.css new file mode 100644 index 0000000..ed9acf2 --- /dev/null +++ b/apps/website/app/app.css @@ -0,0 +1,9 @@ +@import "tailwindcss"; +@import "fumadocs-ui/css/neutral.css"; +@import "fumadocs-ui/css/preset.css"; + +@layer base { + body { + font-family: "Inter", sans-serif; + } +} diff --git a/apps/website/src/components/docs/config-viewer.tsx b/apps/website/app/components/docs/config-viewer.tsx similarity index 99% rename from apps/website/src/components/docs/config-viewer.tsx rename to apps/website/app/components/docs/config-viewer.tsx index 053e776..5adf22d 100644 --- a/apps/website/src/components/docs/config-viewer.tsx +++ b/apps/website/app/components/docs/config-viewer.tsx @@ -1,5 +1,3 @@ -'use client'; - import { ChevronDown, ChevronRight, Search, X } from 'lucide-react'; import type React from 'react'; import { useMemo, useState } from 'react'; @@ -77,7 +75,7 @@ const ConfigViewer = ({ config, comments = {}, title = 'Configuration' }: Config const renderComment = (comment?: string, path?: string, depth: number = 0) => { if (!comment || !path || !visibleComments.has(path)) return null; - const indentSpaces = ' '.repeat(depth); + const _indentSpaces = ' '.repeat(depth); return (
{ window.open(build.downloads.application.url, '_blank'); }; diff --git a/apps/website/src/components/downloads/atlas-builds-list.tsx b/apps/website/app/components/downloads/atlas-builds-list.tsx similarity index 91% rename from apps/website/src/components/downloads/atlas-builds-list.tsx rename to apps/website/app/components/downloads/atlas-builds-list.tsx index bf66681..a3fbacf 100644 --- a/apps/website/src/components/downloads/atlas-builds-list.tsx +++ b/apps/website/app/components/downloads/atlas-builds-list.tsx @@ -1,11 +1,9 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { AlertCircle, AlertTriangle, FlaskConical, Loader2, XCircle } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router'; +import { type Build, fetchBuilds, type VersionWithBuilds } from '@/lib/atlas'; import { AtlasBuildCard } from './atlas-build-card'; import { VersionSelector } from './version-selector'; -import { Loader2, AlertCircle, AlertTriangle, XCircle, FlaskConical } from 'lucide-react'; -import { type Build, type VersionWithBuilds, fetchBuilds } from '@/lib/atlas'; interface AtlasBuildsListProps { projectId: string; @@ -24,8 +22,7 @@ export function AtlasBuildsList({ versionsMetadata, experimentalVersion, }: AtlasBuildsListProps) { - const router = useRouter(); - const searchParams = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const urlVersion = searchParams.get('version'); const initialSelectedVersion = urlVersion && initialVersions.includes(urlVersion) ? urlVersion : defaultVersion; @@ -43,9 +40,13 @@ export function AtlasBuildsList({ const handleVersionChange = (version: string) => { setSelectedVersion(version); - const params = new URLSearchParams(searchParams.toString()); - params.set('version', version); - router.push(`?${params.toString()}`, { scroll: false }); + setSearchParams( + prev => { + prev.set('version', version); + return prev; + }, + { preventScrollReset: true }, + ); }; useEffect(() => { diff --git a/apps/website/src/components/downloads/version-selector.tsx b/apps/website/app/components/downloads/version-selector.tsx similarity index 97% rename from apps/website/src/components/downloads/version-selector.tsx rename to apps/website/app/components/downloads/version-selector.tsx index a416d00..c146f1e 100644 --- a/apps/website/src/components/downloads/version-selector.tsx +++ b/apps/website/app/components/downloads/version-selector.tsx @@ -1,10 +1,8 @@ -'use client'; - -import { useState } from 'react'; -import { ChevronDown, Check, AlertTriangle, XCircle, FlaskConical } from 'lucide-react'; -import { cn } from '@/lib/utils'; import * as Popover from '@radix-ui/react-popover'; +import { AlertTriangle, Check, ChevronDown, FlaskConical, XCircle } from 'lucide-react'; +import { useState } from 'react'; import type { VersionWithBuilds } from '@/lib/atlas'; +import { cn } from '@/lib/cn'; interface VersionSelectorProps { versions: string[]; diff --git a/apps/website/src/components/icons.tsx b/apps/website/app/components/icons.tsx similarity index 100% rename from apps/website/src/components/icons.tsx rename to apps/website/app/components/icons.tsx diff --git a/apps/website/app/components/mdx.tsx b/apps/website/app/components/mdx.tsx new file mode 100644 index 0000000..c7d0113 --- /dev/null +++ b/apps/website/app/components/mdx.tsx @@ -0,0 +1,25 @@ +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import type { MDXComponents } from 'mdx/types'; +import ConfigViewer from '@/components/docs/config-viewer'; + +export function getMDXComponents(components?: MDXComponents) { + return { + ...defaultMdxComponents, + ...components, + Step, + Steps, + Callout, + Tab, + Tabs, + ConfigViewer, + } satisfies MDXComponents; +} + +export const useMDXComponents = getMDXComponents; + +declare global { + type MDXProvidedComponents = ReturnType; +} diff --git a/apps/website/src/components/sections/community.tsx b/apps/website/app/components/sections/community.tsx similarity index 99% rename from apps/website/src/components/sections/community.tsx rename to apps/website/app/components/sections/community.tsx index 4381e79..2a147f6 100644 --- a/apps/website/src/components/sections/community.tsx +++ b/apps/website/app/components/sections/community.tsx @@ -1,5 +1,3 @@ -'use client'; - import { Github, MessageCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; diff --git a/apps/website/src/components/sections/footer.tsx b/apps/website/app/components/sections/footer.tsx similarity index 90% rename from apps/website/src/components/sections/footer.tsx rename to apps/website/app/components/sections/footer.tsx index cf62881..010067b 100644 --- a/apps/website/src/components/sections/footer.tsx +++ b/apps/website/app/components/sections/footer.tsx @@ -1,10 +1,6 @@ -'use client'; - -import Image from 'next/image'; -import Link from 'next/link'; - +import { Link } from 'react-router'; +import { DiscordIcon, GitHubIcon } from '@/components/icons'; import { siteConfig } from '@/config/site'; -import { DiscordIcon, GitHubIcon } from '../icons'; interface LinkItem { href: string; @@ -59,11 +55,11 @@ function LinkColumn({ title, links }: LinkColumnProps) { return (

{title}

-
    +
      {links.map(({ href, label }) => { const isExternal = href.startsWith('http'); return ( -
    • +
    • {isExternal ? ( ) : ( - + {label} )} @@ -95,10 +91,10 @@ export function Footer() { diff --git a/apps/website/src/components/sections/projects.tsx b/apps/website/app/components/sections/projects.tsx similarity index 82% rename from apps/website/src/components/sections/projects.tsx rename to apps/website/app/components/sections/projects.tsx index e455e2e..db465db 100644 --- a/apps/website/src/components/sections/projects.tsx +++ b/apps/website/app/components/sections/projects.tsx @@ -1,8 +1,7 @@ -import { BookOpen, Download, Github, ExternalLink } from 'lucide-react'; - +import { BookOpen, Download, Github } from 'lucide-react'; +import { Link } from 'react-router'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; -import Link from 'next/link'; type Project = { name: string; @@ -40,6 +39,8 @@ const projects: Project[] = [ ]; function ProjectCard({ name, description, downloadUrl, docUrl, sourceUrl }: Project) { + const isExternalDownload = downloadUrl.startsWith('http'); + return (
      @@ -51,16 +52,29 @@ function ProjectCard({ name, description, downloadUrl, docUrl, sourceUrl }: Proj
      + ), + }} + > + {clientLoader.useContent(path, { lastModifiedTime })} + + ); +} diff --git a/apps/website/app/routes/downloads.$project.tsx b/apps/website/app/routes/downloads.$project.tsx new file mode 100644 index 0000000..dfde865 --- /dev/null +++ b/apps/website/app/routes/downloads.$project.tsx @@ -0,0 +1,198 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { ArrowLeft, BookOpen, Download, Info } from 'lucide-react'; +import { Link, useLoaderData } from 'react-router'; +import { AtlasBuildsList } from '@/components/downloads/atlas-builds-list'; +import { GitHubIcon } from '@/components/icons'; +import { Button } from '@/components/ui/button'; +import { + fetchLatestBuild, + fetchProject, + fetchVersions, + formatFileSize, + getAllVersions, + type VersionWithBuilds, +} from '@/lib/atlas'; +import { baseOptions } from '@/lib/layout.shared'; +import type { Route } from './+types/downloads.$project'; + +export async function loader({ params }: Route.LoaderArgs) { + const projectId = params.project; + + try { + const projectData = await fetchProject(projectId); + const versions = getAllVersions(projectData.version_groups); + + let versionsMetadata: VersionWithBuilds[] = []; + try { + versionsMetadata = await fetchVersions(projectId); + } catch {} + + let latestBuild = null; + if (projectData.project.latestVersion) { + try { + latestBuild = await fetchLatestBuild(projectId, projectData.project.latestVersion); + } catch {} + } + + return { projectData, versions, versionsMetadata, latestBuild, projectId }; + } catch { + throw new Response('Not found', { status: 404 }); + } +} + +export function meta({ data }: Route.MetaArgs) { + if (!data) return [{ title: 'Project Not Found | BX Team' }]; + return [ + { title: `${data.projectData.project.name} Downloads | BX Team` }, + { name: 'description', content: `Download the latest ${data.projectData.project.name} builds` }, + ]; +} + +export default function ProjectDownloadsPage() { + const { projectData, versions, versionsMetadata, latestBuild, projectId } = useLoaderData(); + + const githubUrl = `https://github.com/BX-Team/${projectData.project.name}`; + const docsUrl = `/docs/${projectId}`; + + return ( + +
      +
      +
      +
      + +
      +
      + +
      + +
      +
      +
      +

      {projectData.project.name}

      +

      + Get the latest builds of {projectData.project.name} for your Minecraft server +

      + + {latestBuild && ( +
      +
      +
      Latest Build
      +
      #{latestBuild.id}
      +
      +
      +
      Channel
      +
      {latestBuild.channel}
      +
      +
      +
      File Size
      +
      + {formatFileSize(latestBuild.downloads.application.size)} +
      +
      +
      +
      Updated
      +
      + {new Date(latestBuild.time).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} +
      +
      +
      + )} + +
      + {latestBuild && ( + + )} + + +
      +
      + +
      +
      +

      + + Latest Build Info +

      +
      + {latestBuild ? ( +
      +
      Build #{latestBuild.id} Changes
      +
      + {latestBuild.commits.map(commit => ( +
      + +
      + + {commit.sha.substring(0, 7)} + +

      {commit.message}

      +
      +
      + ))} +
      +
      + ) : ( +
      No build information available
      + )} +
      +
      +
      +
      +
      + +
      +

      All Builds

      + {versions.length > 0 ? ( +
      + +
      + ) : ( +
      +
      + +

      No Versions Available

      +

      + There are currently no {projectData.project.name} builds available. +

      +
      +
      + )} +
      +
      +
      + + ); +} diff --git a/apps/website/app/routes/downloads.tsx b/apps/website/app/routes/downloads.tsx new file mode 100644 index 0000000..f2eff06 --- /dev/null +++ b/apps/website/app/routes/downloads.tsx @@ -0,0 +1,213 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { ArrowRight, Clock, Download, GitCommit, Package } from 'lucide-react'; +import { Link, useLoaderData } from 'react-router'; +import { Button } from '@/components/ui/button'; +import { fetchLatestBuild, fetchProjects, formatFileSize, getAllVersions, getChannelColor } from '@/lib/atlas'; +import { baseOptions } from '@/lib/layout.shared'; +import type { Route } from './+types/downloads'; + +export async function loader(_args: Route.LoaderArgs) { + try { + const response = await fetchProjects(); + + const projectsWithBuilds = await Promise.all( + response.projects.map(async project => { + const versions = getAllVersions(project.version_groups); + const latestVersion = project.project.latestVersion || versions[0]; + + if (latestVersion) { + try { + const latestBuild = await fetchLatestBuild(project.project.id, latestVersion); + return { ...project, latestBuild }; + } catch { + return { ...project, latestBuild: null }; + } + } + return { ...project, latestBuild: null }; + }), + ); + + return { projects: projectsWithBuilds, error: null }; + } catch (err) { + return { projects: [], error: err instanceof Error ? err.message : 'Failed to load projects' }; + } +} + +export function meta(_args: unknown) { + return [ + { title: 'Downloads | BX Team' }, + { name: 'description', content: 'Download the latest builds of our Minecraft server software' }, + ]; +} + +export default function DownloadsPage() { + const { projects, error } = useLoaderData(); + + return ( + +
      +
      +
      +
      + +
      +
      +

      Downloads

      +

      Select software you want to download

      +
      + +
      + {error ? ( +
      +
      +
      + + + +
      +

      Failed to Load Projects

      +

      {error}

      +
      +
      + ) : projects.length > 0 ? ( +
      + {projects.map((project: any) => ( +
      +
      +
      +
      + +

      {project.project.name}

      + +

      + {project.project.description || 'No description available.'} +

      +
      +
      + + {project.latestBuild && ( +
      +
      +
      Latest Build
      +
      #{project.latestBuild.id}
      +
      +
      +
      Channel
      +
      + + {project.latestBuild.channel} + +
      +
      +
      +
      Size
      +
      + + {formatFileSize(project.latestBuild.downloads.application.size)} +
      +
      +
      +
      Updated
      +
      + + {new Date(project.latestBuild.time).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + })} +
      +
      +
      + )} +
      + + {project.latestBuild && project.latestBuild.commits.length > 0 && ( +
      +
      + +

      + Recent Changes +

      +
      +
      + {project.latestBuild.commits.slice(0, 3).map((commit: any) => ( +
      + +
      + + {commit.sha.substring(0, 7)} + +

      {commit.message}

      +
      +
      + ))} + {project.latestBuild.commits.length > 3 && ( +

      + +{project.latestBuild.commits.length - 3} more commits +

      + )} +
      +
      + )} + +
      + {project.latestBuild && ( + + )} + +
      +
      + ))} +
      + ) : ( +
      +
      + +

      No Projects Available

      +

      + There are currently no projects available for download. +

      +
      +
      + )} +
      +
      +
      + + ); +} diff --git a/apps/website/app/routes/home.tsx b/apps/website/app/routes/home.tsx new file mode 100644 index 0000000..0df2af7 --- /dev/null +++ b/apps/website/app/routes/home.tsx @@ -0,0 +1,28 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { Community } from '@/components/sections/community'; +import { Footer } from '@/components/sections/footer'; +import { Hero } from '@/components/sections/hero'; +import { Projects } from '@/components/sections/projects'; +import { baseOptions } from '@/lib/layout.shared'; + +export function meta(_args: unknown) { + return [ + { title: 'BX Team' }, + { + name: 'description', + content: + 'BX Team is an open source community that focuses on development and maintenance of high-quality Minecraft server software.', + }, + ]; +} + +export default function Home() { + return ( + + + + +