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
6 changes: 3 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- run: npm install
- run: npm run lint
- run: npm run build
Expand All @@ -21,7 +21,7 @@ jobs:
# - uses: actions/checkout@v3
# - uses: actions/setup-node@v3
# with:
# node-version: 18.x
# node-version: 22.x
# - run: npm install
# - name: unit test
# run: npm run test:unit
Expand All @@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- run: npm install
- name: e2e testing
run: npm run test:e2e
8 changes: 4 additions & 4 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- run: npm install
- run: npm run lint
- run: npm run build
Expand All @@ -21,7 +21,7 @@ jobs:
# - uses: actions/checkout@v3
# - uses: actions/setup-node@v3
# with:
# node-version: 18.x
# node-version: 22.x
# - run: npm install
# - name: unit test
# run: npm run test:unit
Expand All @@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- run: npm install
- name: unit test
run: npm run test:e2e
Expand All @@ -44,7 +44,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 22.x
- name: Install plugins
run: |
npm install @semantic-release/commit-analyzer -D
Expand Down
32 changes: 10 additions & 22 deletions app/(home)/DownloadButtonClient.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
'use client';
import LinkButton from "../common/uiLibrary/linkButton";
import {useEffect, useState} from "react";
import getDownloadLink from "../../utils/platform";
import {GITHUB_RELEASES_URL} from "../../utils/urlContants";
import {BiSolidDownload} from "react-icons/bi";


import LinkButton from '../common/uiLibrary/linkButton';
import { BiSolidDownload } from 'react-icons/bi';

const DownloadButtonClient = () => {
const [downloadLink, setDownloadLink] = useState(GITHUB_RELEASES_URL);

useEffect(() => {
getDownloadLink().then((link) => setDownloadLink(link));
}, []);

return (
<div className={'flex flex-col lg:flex-row justify-center mt-8 gap-4'}>
<LinkButton className="shadow-2xl" filled={true} text={'Download'} linkTo={downloadLink} iconRight={BiSolidDownload} />
</div>
)
}

export default DownloadButtonClient;
return (
<div className={'flex flex-col lg:flex-row justify-center mt-8 gap-4'}>
<LinkButton className="shadow-2xl" filled={true} text={'Download'} linkTo={'/download'} iconRight={BiSolidDownload} />
</div>
);
};

export default DownloadButtonClient;
20 changes: 10 additions & 10 deletions app/(home)/HomeBannerClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import layoutChildren from "../../types/layoutChildren";
import {useEffect, useState} from "react";

const images: string[] = [
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/bar-engine.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/clowns.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/conveyor.jpg",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/df.jpg",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/go-outsid.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/honk.jpg",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/hugger.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/lemons.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/shuttlecrash.png",
"https://unitystationfile.b-cdn.net/Website-Statics/heroImages/chairs.jpg",
"https://cdn.unitystation.org/Website-Statics/heroImages/bar-engine.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/clowns.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/conveyor.jpg",
"https://cdn.unitystation.org/Website-Statics/heroImages/df.jpg",
"https://cdn.unitystation.org/Website-Statics/heroImages/go-outsid.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/honk.jpg",
"https://cdn.unitystation.org/Website-Statics/heroImages/hugger.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/lemons.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/shuttlecrash.png",
"https://cdn.unitystation.org/Website-Statics/heroImages/chairs.jpg",
];

let currentIndex = 0;
Expand Down
2 changes: 1 addition & 1 deletion app/changelog/buildComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const DownloadBuildDropdown = (props: { version: string }) => {
{platforms.map((platform) => (
<a
key={platform}
href={`https://unitystationfile.b-cdn.net/UnityStationDevelop/${platform}/${props.version}.zip`}
href={`https://cdn.unitystation.org/UnityStationDevelop/${platform}/${props.version}.zip`}
className="block px-4 py-2 text-sm text-blue-50 hover:bg-gray-100 hover:text-gray-900"
role="menuitem"
onClick={handleClick}
Expand Down
15 changes: 15 additions & 0 deletions app/download/fetchLatestRelease.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import GithubReleaseResponse from '../../types/githubReleaseResponse';
import { LAUNCHER_RELEASES_API } from '../../utils/urlContants';

async function fetchLatestRelease(): Promise<GithubReleaseResponse> {
const response = await fetch(LAUNCHER_RELEASES_API, {
next: { revalidate: 3600 },
headers: { Accept: 'application/vnd.github+json' },
});
if (!response.ok) {
throw new Error(`GitHub releases API responded ${response.status}`);
}
return response.json();
}

export default fetchLatestRelease;
53 changes: 53 additions & 0 deletions app/download/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { headers } from 'next/headers';
import PageHeading from '../common/uiLibrary/PageHeading';
import { LauncherRelease } from '../../types/launcherRelease';
import { GITHUB_RELEASES_URL, LAUNCHER_REPO_URL } from '../../utils/urlContants';
import mapLauncherRelease from '../../utils/launcherRelease';
import { detectPlatform } from '../../utils/detectPlatform';
import fetchLatestRelease from './fetchLatestRelease';
import DownloadExperience from '../../components/organisms/DownloadExperience';
import QuietExternalLink from '../../components/atoms/QuietExternalLink';

export const metadata = {
title: 'Download · Unitystation',
description: 'Download the Pudu Launcher and start playing Unitystation.',
};

const DownloadPage = async () => {
let release: LauncherRelease | null = null;
try {
release = mapLauncherRelease(await fetchLatestRelease());
} catch {
release = null;
}

// Detect the platform on the server from the request so the right OS is in the
// first paint. The client effect only re-runs when this comes back unknown.
const userAgent = (await headers()).get('user-agent') ?? undefined;
const platform = detectPlatform(userAgent);
const initialOs = platform.os === 'unknown' ? undefined : platform.os;

return (
<div className="flex flex-col items-center text-center px-4 py-12">
<PageHeading isCentered>Download Pudu Launcher</PageHeading>
<p className="max-w-xl text-gray-400">
The Pudu Launcher keeps your game up to date and gets you into a round quickly.
We&apos;ve highlighted the build for your system below.
</p>

<DownloadExperience
release={release}
fallbackUrl={GITHUB_RELEASES_URL}
initialOs={initialOs}
initialLinuxFamily={platform.linuxFamily}
/>

<QuietExternalLink href={LAUNCHER_REPO_URL} className="mt-10">
View the launcher source on GitHub
</QuietExternalLink>
</div>
);
};

export default DownloadPage;
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ html, body {
}

body {
background-image: url("https://unitystationfile.b-cdn.net/Website-Statics/layer1.png");
background-image: url("https://cdn.unitystation.org/Website-Statics/layer1.png");
}

a {
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const metadata: Metadata = {
description: 'Unitystation is a free and open-source chaotic multiplayer role-playing and simulation game made in Unity. Remake of the cult classic Space Station 13.',
images: [
{
url: 'https://unitystationfile.b-cdn.net/Branding/US13_OG_image_preview_1.png',
url: 'https://cdn.unitystation.org/Branding/US13_OG_image_preview_1.png',
},
]
}
Expand Down
16 changes: 16 additions & 0 deletions components/atoms/AltDownloadLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DownloadVariant } from '../../types/launcherRelease';

// Outline Button tokens for secondary downloads, so alternatives read as real
// buttons (not decorative cards) while staying quieter than the primary action.
function AltDownloadLink({ variant }: { variant: DownloadVariant }) {
return (
<a
href={variant.url}
className="uppercase flex items-center justify-center gap-2 py-2 px-4 bg-gray-800 bg-opacity-30 border-2 border-gray-800 text-white transition-colors hover:bg-gray-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900"
>
{variant.label} · {variant.sizeMB} MB
</a>
);
}

export default AltDownloadLink;
33 changes: 33 additions & 0 deletions components/atoms/CopyableCommand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';
import { useState } from 'react';
import { FiCopy, FiCheck } from 'react-icons/fi';

function CopyableCommand({ command }: { command: string }) {
const [copied, setCopied] = useState(false);

const onCopy = async () => {
try {
await navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard unavailable (e.g. insecure context); the command stays selectable.
}
};

return (
<div className="flex items-center justify-center gap-2 normal-case">
<code className="text-xs text-gray-300 bg-gray-800 rounded px-2 py-1 select-all">{command}</code>
<button
type="button"
onClick={onCopy}
aria-label={copied ? 'Command copied' : 'Copy install command'}
className="p-1 rounded text-gray-400 transition-colors hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
>
{copied ? <FiCheck className="w-4 h-4" aria-hidden /> : <FiCopy className="w-4 h-4" aria-hidden />}
</button>
</div>
);
}

export default CopyableCommand;
12 changes: 12 additions & 0 deletions components/atoms/CornerBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Capsule from '../../app/common/uiLibrary/capsule';

// The parent must be positioned (`relative`) for the absolute corner placement.
function CornerBadge({ text }: { text: string }) {
return (
<span className="absolute -top-2 right-2 normal-case">
<Capsule text={text} colour="blue" />
</span>
);
}

export default CornerBadge;
18 changes: 18 additions & 0 deletions components/atoms/HeroDownloadLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BiSolidDownload } from 'react-icons/bi';
import { DownloadVariant } from '../../types/launcherRelease';

// Mirrors the filled Button/LinkButton tokens (gray-800, square, uppercase) so the
// primary download reads as part of the site rather than a one-off blue CTA.
function HeroDownloadLink({ variant }: { variant: DownloadVariant }) {
return (
<a
href={variant.url}
className="uppercase flex items-center justify-center gap-3 py-3 px-6 text-lg bg-gray-800 border-2 border-transparent text-white transition-colors hover:bg-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900"
>
<BiSolidDownload className="w-7 h-7" aria-hidden />
<span>Download · {variant.label}</span>
</a>
);
}

export default HeroDownloadLink;
23 changes: 23 additions & 0 deletions components/atoms/QuietExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ReactNode } from 'react';

type Props = {
href: string;
children: ReactNode;
className?: string;
};

// Opens in a new tab so it never pulls the user away from what they were doing.
function QuietExternalLink({ href, children, className = '' }: Props) {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className={`text-xs text-gray-400 underline transition-colors hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded-sm ${className}`}
>
{children}
</a>
);
}

export default QuietExternalLink;
34 changes: 34 additions & 0 deletions components/molecules/OsSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';
import { OsKey } from '../../types/launcherRelease';
import Capsule from '../../app/common/uiLibrary/capsule';
import { OS_META } from '../osMeta';

type Props = {
options: OsKey[];
detected: OsKey | null;
onSelect: (os: OsKey) => void;
};

export default function OsSwitcher({ options, detected, onSelect }: Props) {
return (
<div className="flex items-center gap-4 text-sm text-gray-400">
<span>On a different OS?</span>
{options.map((os) => {
const { label, Icon } = OS_META[os];
return (
<button
key={os}
type="button"
onClick={() => onSelect(os)}
aria-label={`Show ${label} downloads`}
className="inline-flex items-center gap-1.5 px-2 py-1 rounded uppercase text-gray-400 transition-colors hover:text-white hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900"
>
<Icon className="w-4 h-4" aria-hidden />
<span>{label}</span>
{os === detected && <Capsule text="Detected" colour="blue" />}
</button>
);
})}
</div>
);
}
Loading
Loading