Skip to content
Draft
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
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
"@astrojs/sitemap": "3.7.3",
"@astrojs/svelte": "9.0.1",
"@fontsource/poppins": "5.2.7",
"@iconify-json/file-icons": "^1.2.2",
"@iconify-json/flowbite": "^1.2.7",
"@iconify-json/griddy-icons": "^1.2.0",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/pinhead": "^1.2.13",
"@iconify-json/tabler": "^1.2.38",
"@iconify-json/wpf": "^1.2.0",
"@tailwindcss/vite": "4.3.3",
"astro": "7.1.1",
"astro-iconset": "^0.0.6",
Expand Down
70 changes: 70 additions & 0 deletions src/components/data/DropdownSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script lang="ts">
import Icon from "astro-iconset/svelte";

interface Option {
name: string;
icon?: string;
}

interface Props {
value?: string;
options: Option[];
}

let { value = $bindable(), options = [] }: Props = $props();
let currentOption = $derived(options.find((o) => o.name == (value ?? options[0].name))!!);

let dropDown = $state(false);

function selectOption(opt: Option) {
dropDown = false;
currentOption = opt;
value = opt.name;
}

function handleOutsideClick(event: any) {
if (dropDown && !(event.target?.closest(".dropdown-clickable") ?? false)) {
dropDown = false;
}
}
</script>

<svelte:window onclick={handleOutsideClick} />

<div class="relative">
<button
class="dropdown-clickable flex w-fit cursor-pointer items-center justify-center gap-2 rounded-md border-2
border-gray-700 px-2 transition-colors duration-150 ease-in-out hover:bg-gray-600"
onclick={(e) => (dropDown = !dropDown)}
>
{#if currentOption.icon}
<Icon name={currentOption.icon} />
{/if}
{currentOption.name}

<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-100" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>

{#if dropDown}
<div class="border-b-background-dark-80 bg-background-dark-90 absolute z-10 w-fit rounded-md border-2 object-contain">
{#each options as option (option.name)}
<button
class="dropdown-clickable flex w-full cursor-pointer items-center gap-2
px-2 py-1 transition-colors duration-150 ease-in-out hover:bg-gray-600"
onclick={(e) => selectOption(option)}
>
{#if option.icon}
<Icon name={option.icon} />
{/if}
{option.name}
</button>
{/each}
</div>
{/if}
</div>
91 changes: 91 additions & 0 deletions src/components/data/TackDownloadButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<script lang="ts">
import { onMount } from "svelte";

interface Props {
repository: string;
version: string;
asset: string;
compact?: boolean;
eol?: boolean;
disabled?: boolean;
}

let { repository, version, asset, compact = false, eol = false, disabled = false }: Props = $props();

let url = $derived(`https://github.com/${repository}/releases/download/${version}/${asset}`);

let open = $state(false);
let rootEl: HTMLDivElement | null = $state(null);
let copied: Record<string, boolean> = $state({});

function close() {
open = false;
}

function toggle() {
if (!disabled) open = !open;
}

function onDocumentClick(ev: MouseEvent) {
const t = ev.target as HTMLElement | null;
if (!rootEl) return;
if (t && rootEl.contains(t)) return;
close();
}

onMount(() => {
document.addEventListener("click", onDocumentClick);
return () => document.removeEventListener("click", onDocumentClick);
});

type DownloadEntry = [string, { name: string; checksums: { sha256: string }; size: number; url: string }];

async function copySha256(evt: MouseEvent, entry: DownloadEntry) {
evt.preventDefault();
evt.stopPropagation();
const [, d] = entry;
if (!d.checksums.sha256) return;
try {
await navigator.clipboard.writeText(d.checksums.sha256);
copied[d.name] = true;
copied = { ...copied };
setTimeout(() => {
copied[d.name] = false;
copied = { ...copied };
}, 2000);
} catch (error) {
console.error("Failed to copy SHA256 checksum to clipboard:", error);
}
}
</script>

<div class="relative flex w-full flex-row md:w-100" bind:this={rootEl}>
<div
class={`btn transition-color flex flex-row rounded-md
${!compact ? "w-full md:w-100" : ""}
${eol ? "btn-eol" : `btn-stable`}
${disabled ? "cursor-not-allowed opacity-60" : ""}`}
>
<a
class={`flex flex-1 flex-row items-center ${compact ? "gap-2 py-1 pl-2 leading-0" : "gap-8 py-3 pl-5"}`}
href={url}
target="_blank"
aria-disabled={disabled}
onclick={(e) => disabled && (e.preventDefault(), e.stopPropagation())}
>
<div class={compact ? "h-4 w-4" : "h-8 w-8"}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>

<div class="flex-1 pr-2 text-left">
<span class="text-sm font-medium">Download</span>
</div>
</a>
</div>
</div>
92 changes: 92 additions & 0 deletions src/components/data/TackDownloadPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script lang="ts">
import TackDownloadButton from "@/components/data/TackDownloadButton.svelte";
import { onMount } from "svelte";
import DropdownSelect from "@/components/data/DropdownSelect.svelte";
import Icon from "astro-iconset/svelte";

interface Props {
description?: string;
experimentalWarning?: boolean;
eol?: boolean;
repository: string;
}

let { description = undefined, experimentalWarning = undefined, eol = false, repository }: Props = $props();

let icon = undefined;
let version = "vUnknown";

// Detect user OS and CPU architecture for default download.
let os: "Windows" | "MacOS" | "Linux" | undefined = $state(undefined);
let cpu: "x64" | "arm64" | undefined = $state(undefined);

onMount(() => {
const userAgent = navigator.userAgent.toLowerCase();

if (userAgent.includes("win")) {
os = "Windows";
} else if (userAgent.includes("mac")) {
os = "MacOS";
} else if (userAgent.includes("linux")) {
os = "Linux";
}

if (userAgent.includes("aarch64") || userAgent.includes("arm64") || userAgent.concat("armv8")) {
cpu = "arm64";
}
if (userAgent.includes("x86")) {
cpu = "x64";
}
});

interface OptionWithIcon {
name: string;
icon?: string;
}

const osOptions: OptionWithIcon[] = [
{ name: "Windows", icon: "flowbite:windows-solid" },
{ name: "Linux", icon: "file-icons:arch-linux" },
{ name: "MacOS", icon: "wpf:macos" },
];

const cpuOptions: OptionWithIcon[] = [
{ name: "x64", icon: "mdi:cpu-64-bit" },
{ name: "arm64", icon: "file-icons:arm" },
];
</script>

<header class="mx-auto flex max-w-7xl flex-row flex-wrap gap-16 px-4 pt-32 pb-16 lg:pt-48 lg:pb-26">
<div class="flex-1">
<div class="mb-6 flex flex-row items-center gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-800 p-3">
<Icon name="pinhead:flat-head-thumb-tack" class="h-full w-full text-white" />
</div>
<h1 class="text-xl font-medium">Downloads</h1>
</div>

<h2 class="text-4xl leading-normal font-medium lg:text-5xl lg:leading-normal">
Get Tack
<span class="stable">{version}</span>
</h2>

<p class="mt-4 text-xl">Before downloading, ensure your operating system and CPU architecture match!</p>

<div class="mt-4 flex gap-2 text-xl">
Currently selected:
<DropdownSelect bind:value={os} options={osOptions} />
on
<DropdownSelect bind:value={cpu} options={cpuOptions} />
</div>

<div class="mt-8 flex flex-col gap-4">
<TackDownloadButton {repository} {version} asset="none" eol={!!eol} disabled={false} />
</div>

<section id="builds" class="mt-20">
<!-- <SoftwareBuilds project={id} {version} builds={builds.value.builds} eol={!!eol} />-->
</section>

<div class="hidden"></div>
</div>
</header>
1 change: 1 addition & 0 deletions src/components/layout/NavBar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import LogoMarkerLight from "@/assets/brand/logo-marker-light.svg";
<NavDropDownLink href="/software/paper">Paper</NavDropDownLink>
<NavDropDownLink href="/software/folia">Folia</NavDropDownLink>
<NavDropDownLink href="/software/velocity">Velocity</NavDropDownLink>
<NavDropDownLink href="/software/tack">Tack</NavDropDownLink>
<NavDropDownLink href="/software/waterfall" eol>Waterfall</NavDropDownLink>
</NavDropDown>
<NavLink href="https://hangar.papermc.io/" target="_blank" className="inline-flex items-center">
Expand Down
10 changes: 10 additions & 0 deletions src/pages/downloads/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,15 @@ import Layout from "@/layouts/Layout.astro";
eol
/>
</div>
<h2 class="mt-4 text-center text-2xl font-medium">Misc</h2>
<div class="grid gap-2 px-2 md:grid-cols-2 xl:gap-4">
<SoftwarePreview
id="tack"
name="Tack"
icon="pinhead:flat-head-thumb-tack"
description="Tack is our AOT-ready Paper JAR launcher for reduced startup time."
type={SoftwarePreviewType.Download}
/>
</div>
</header>
</Layout>
17 changes: 17 additions & 0 deletions src/pages/downloads/tack.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
export const prerender = false;
import TackDownloadPage from "@/components/data/TackDownloadPage.svelte";
import Layout from "@/layouts/Layout.astro";

Astro.response.headers.set("Cache-Control", "public, max-age=300, s-maxage=600, stale-while-revalidate=120");
Astro.response.headers.set("CDN-Cache-Control", "public, max-age=600, stale-while-revalidate=120");
---

<Layout
title="Tack"
description="Download Tack, our AOT-ready Paper JAR launcher for reduced startup time."
keywords={["papermc", "minecraft", "performance", "tack", "downloads", "windows", "linux", "mac"]}
canonical="/downloads/tack"
>
<TackDownloadPage client:load repository="PaperMC/Tack" />
</Layout>
30 changes: 30 additions & 0 deletions src/pages/software/tack.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
import Layout from "@/layouts/Layout.astro";
import SoftwareHeader from "@/components/layout/SoftwareHeader.astro";
import FeatureCard from "@/components/data/FeatureCard.astro";
import Button from "@/components/input/Button.astro";

import HomeImage1 from "@/assets/images/home-1.png";
import HomeImage3 from "@/assets/images/home-3.png";
import CommunityImage from "@/assets/images/community.png";

import { Image } from "astro:assets";

import { getHangarProjects } from "@/utils/hangar";

const { pagination } = await getHangarProjects("paper");
---

<Layout
title="Tack"
description="Tack is our AOT-ready Paper JAR launcher for reduced startup time."
keywords={["papermc", "minecraft", "performance", "tack", "windows", "linux", "mac"]}
canonical="/software/tack"
>
<SoftwareHeader id="tack" name="Tack" icon="pinhead:flat-head-thumb-tack">
<p slot="header">
Improved startup-time<br /><span class="text-blue-500">for Paper</span>
</p>
<p slot="description">Tack is our AOT-ready Paper JAR launcher for reduced startup time.</p>
</SoftwareHeader>
</Layout>
Loading