Compare commits

..

7 Commits

Author SHA1 Message Date
Calum H.
475ee69cfb feat: "no servers found" impl 2025-08-06 16:58:36 +01:00
IMB11
ab48d4b144 Merge branch 'main' into cal/server-panel-refactor 2025-08-05 11:26:41 +01:00
IMB11
3480448351 Merge branch 'main' into cal/server-panel-refactor 2025-08-04 17:26:00 +01:00
IMB11
daae37806c fix: update button size for new server action 2025-07-28 19:14:23 +01:00
IMB11
6a4c140420 refactor: remove mobile pull refresh logic 2025-07-28 18:47:53 +01:00
IMB11
5d98b16270 feat: manage page 2025-07-28 18:43:00 +01:00
IMB11
6c452f86b6 feat: hide globe behind flag 2025-07-28 17:30:22 +01:00
23 changed files with 512 additions and 1156 deletions

View File

@@ -76,10 +76,10 @@ const installing = ref(false)
const onInstall = ref(() => {})
defineExpose({
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
show: (instanceVal, projectVal, projectVersions, callback) => {
instance.value = instanceVal
versions.value = projectVersions
selectedVersion.value = selected ?? projectVersions[0]
selectedVersion.value = projectVersions[0]
project.value = projectVal

View File

@@ -29,8 +29,8 @@ export const useInstall = defineStore('installStore', {
setIncompatibilityWarningModal(ref) {
this.incompatibilityWarningModal = ref
},
showIncompatibilityWarningModal(instance, project, versions, selected, onInstall) {
this.incompatibilityWarningModal.show(instance, project, versions, selected, onInstall)
showIncompatibilityWarningModal(instance, project, versions, onInstall) {
this.incompatibilityWarningModal.show(instance, project, versions, onInstall)
},
setModInstallModal(ref) {
this.modInstallModal = ref
@@ -133,13 +133,7 @@ export const install = async (
callback(version.id)
} else {
const install = useInstall()
install.showIncompatibilityWarningModal(
instance,
project,
projectVersions,
version,
callback,
)
install.showIncompatibilityWarningModal(instance, project, projectVersions, callback)
}
} else {
const versions = (await get_version_many(project.versions).catch(handleError)).sort(

View File

@@ -3,37 +3,55 @@ title: Labrinth (API)
description: Guide for contributing to Modrinth's backend
---
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory. The instructions below assume that you have switched your working directory to the `apps/labrinth` subdirectory.
This project is part of our [monorepo](https://github.com/modrinth/code). You can find it in the `apps/labrinth` directory.
[Labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
```sh
cargo install sqlx-cli --no-default-features --features mysql,sqlite,postgres,rustls,completions
```bash
cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres,rustls
```
From there, you can create the database and set up its schema with one simple command:
From there, you can create the database and perform all database migrations with one simple command:
```sh
cargo sqlx database setup
```bash
sqlx database setup
```
To enable Labrinth to create projects and serve useful metadata to the frontend build scripts, you'll need to seed the database with several key entities:
To enable labrinth to create a project, you need to add two things.
1. Categories, in the `categories` table.
2. Loaders and their fields, in the `loaders`, `loader_fields`, `loader_field_enums`, `loader_field_enum_values`, and `loader_fields_loaders` tables.
3. Project types and their allowed loaders and games, in the `project_types`, `loaders_project_types`, and `loaders_project_types_games` tables.
4. Optionally, to moderate projects from the frontend, an admin user, in the `users` table.
1. An entry in the `loaders` table.
2. An entry in the `loaders_project_types` table.
The most convenient way to do this seeding is with the [psql](https://www.postgresql.org/docs/current/app-psql.html) command line tool and the pre-existing seed data fixture. This fixture was generated by dumping the official staging environment database at a specific point in time, and defines an admin user with email `admin@modrinth.invalid` and password `admin`:
A minimal setup can be done from the command line with [psql](https://www.postgresql.org/docs/current/app-psql.html):
```sh
source .env
psql "$DATABASE_URL" < fixtures/labrinth-seed-data-202508052143.sql
```bash
psql --host=localhost --port=5432 -U <username, default is labrinth> -W
```
You can find more example SQL statements for seeding the database in the `tests/files/dummy_data.sql` file.
The default password for the database is `labrinth`. Once you've connected, run
```sql
INSERT INTO loaders VALUES (0, 'placeholder_loader');
INSERT INTO loaders_project_types VALUES (0, 1); -- modloader id, supported type id
INSERT INTO categories VALUES (0, 'placeholder_category', 1); -- category id, category, project type id
```
This will initialize your database with a modloader called 'placeholder_loader', with id 0, and marked as supporting mods only. It will also create a category called 'placeholder_category' that is marked as supporting mods only
If you would like 'placeholder_loader' to be marked as supporting modpacks too, run
```sql
INSERT INTO loaders_project_types VALUES (0, 2); -- modloader id, supported type id
```
If you would like 'placeholder_category' to be marked as supporting modpacks too, run
```sql
INSERT INTO categories VALUES (0, 'placeholder_category', 2); -- modloader id, supported type id
```
You can find more example SQL statements for seeding the database in the `apps/labrinth/tests/files/dummy_data.sql` file.
The majority of configuration is done at runtime using [dotenvy](https://crates.io/crates/dotenvy) and the `.env` file. Each of the variables and what they do can be found in the dropdown below. Additionally, there are three command line options that can be used to specify to MeiliSearch what you want to do.
@@ -91,13 +109,14 @@ The OAuth configuration options are fairly self-explanatory. For help setting up
If you're prepared to contribute by submitting a pull request, ensure you have met the following criteria:
- `cargo fmt --all` has been run.
- `cargo clippy --all-targets` has been run.
- `cargo fmt` has been run.
- `cargo clippy` has been run.
- `cargo check` has been run.
- `cargo sqlx prepare` has been run.
> Note: If you encounter issues with `sqlx` saying 'no queries found' after running `cargo sqlx prepare`, you may need to ensure the installed version of `sqlx-cli` matches the current version of `sqlx` used [in labrinth](https://github.com/modrinth/labrinth/blob/master/Cargo.toml).
[Discord]: https://discord.modrinth.com
[GitHub]: https://github.com/modrinth
[Labrinth]: https://github.com/modrinth/code/tree/main/apps/labrinth
[labrinth]: https://github.com/modrinth/labrinth
[Rust]: https://www.rust-lang.org/tools/install

View File

@@ -50,22 +50,27 @@ const container = ref(null);
const showLabels = ref(false);
const locations = ref([
// Active locations
{ name: "New York", lat: 40.7128, lng: -74.006, active: true, clicked: false },
{ name: "Los Angeles", lat: 34.0522, lng: -118.2437, active: true, clicked: false },
{ name: "Miami", lat: 25.7617, lng: -80.1918, active: true, clicked: false },
{ name: "Spokane", lat: 47.667309, lng: -117.411922, active: true, clicked: false },
{ name: "Dallas", lat: 32.78372, lng: -96.7947, active: true, clicked: false },
// Future Locations
// { name: "London", lat: 51.5074, lng: -0.1278, active: false, clicked: false },
// { name: "Frankfurt", lat: 50.1109, lng: 8.6821, active: false, clicked: false },
// { name: "Amsterdam", lat: 52.3676, lng: 4.9041, active: false, clicked: false },
// { name: "Paris", lat: 48.8566, lng: 2.3522, active: false, clicked: false },
// { name: "Singapore", lat: 1.3521, lng: 103.8198, active: false, clicked: false },
// { name: "Tokyo", lat: 35.6762, lng: 139.6503, active: false, clicked: false },
// { name: "Sydney", lat: -33.8688, lng: 151.2093, active: false, clicked: false },
// { name: "São Paulo", lat: -23.5505, lng: -46.6333, active: false, clicked: false },
// { name: "Toronto", lat: 43.6532, lng: -79.3832, active: false, clicked: false },
{
name: "Vint Hill",
lat: 38.74724876915715,
lng: -77.67436507922152,
active: true,
clicked: false,
},
{
name: "Coventry",
lat: 52.39751276904742,
lng: -1.5777183894453757,
active: true,
clicked: false,
},
{
name: "Limburg",
lat: 50.40863558430334,
lng: 8.062427315007714,
active: true,
clicked: false,
},
]);
const isLocationVisible = (location) => {

View File

@@ -34,6 +34,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
showProjectPageDownloadModalServersPromo: false,
showProjectPageCreateServersTooltip: true,
showProjectPageQuickServerButton: false,
showModrinthServersGlobe: false,
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,

View File

@@ -515,6 +515,98 @@
</div>
</section>
<section
v-if="flags.showModrinthServersGlobe"
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
>
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
<div class="mx-auto flex w-full max-w-7xl flex-col gap-8">
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
>
Server Locations
</div>
<h1 class="relative m-0 max-w-2xl text-4xl leading-[120%] md:text-7xl">
Global Coverage
</h1>
</div>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<div class="grid size-8 place-content-center rounded-full bg-highlight-green">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-brand"
>
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>
</div>
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
Strategic Locations
</h2>
</div>
<p
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
>
With servers strategically placed in Vint Hill (USA), Coventry (UK), and Limburg
(Germany), we provide excellent coverage across North America and Europe. Each
location features high-performance hardware and comprehensive DDoS protection.
</p>
</div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<div class="grid size-8 place-content-center rounded-full bg-highlight-blue">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-blue"
>
<path d="M12 2a10 10 0 1 0 10 10" />
<path d="M18 13a6 6 0 0 0-6-6" />
<path d="M13 2.05a10 10 0 0 1 2 2" />
<path d="M19.5 8.5a10 10 0 0 1 2 2" />
</svg>
</div>
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
Low Latency Connectivity
</h2>
</div>
<p
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
>
Our three carefully chosen locations ensure optimal ping times and reliable
connections for players across multiple continents. Choose the region closest to
you for the best gaming experience.
</p>
</div>
</div>
</div>
<Globe />
</div>
</div>
</section>
<section
id="plan"
pyro-hash="plan"
@@ -651,6 +743,7 @@ import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
import OptionGroup from "~/components/ui/OptionGroup.vue";
import Globe from "~/components/ui/servers/Globe.vue";
const { locale } = useVIntl();
@@ -842,6 +935,7 @@ async function fetchPaymentData() {
const selectedProjectId = ref();
const route = useRoute();
const flags = useFeatureFlags();
const isAtCapacity = computed(
() => isSmallAtCapacity.value && isMediumAtCapacity.value && isLargeAtCapacity.value,
);

View File

@@ -0,0 +1,11 @@
<template>
<div
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
>
<ServersManagePage />
</div>
</template>
<script lang="ts" setup>
import { ServersManagePage } from "@modrinth/ui";
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -292,7 +292,7 @@ pub async fn add_dummy_data(api: &ApiV3, db: TemporaryDatabase) -> DummyData {
let pool = &db.pool.clone();
pool.execute(
include_str!("../fixtures/dummy_data.sql")
include_str!("../files/dummy_data.sql")
.replace("$1", &Scopes::all().bits().to_string())
.as_str(),
)

View File

@@ -0,0 +1,56 @@
<svg viewBox="0 0 592 384" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 3C0 1.34315 1.34315 0 3 0H93C94.6569 0 96 1.34315 96 3V93C96 94.6569 94.6569 96 93 96H3C1.34315 96 0 94.6569 0 93V3Z"
fill="#F3F4F6" />
<path
d="M120 18C120 16.8954 120.895 16 122 16H590C591.105 16 592 16.8954 592 18V38C592 39.1046 591.105 40 590 40H122C120.895 40 120 39.1046 120 38V18Z"
fill="#F3F4F6" />
<path
d="M120 62C120 60.8954 120.895 60 122 60H142C143.105 60 144 60.8954 144 62V82C144 83.1046 143.105 84 142 84H122C120.895 84 120 83.1046 120 82V62Z"
fill="#F3F4F6" />
<path
d="M160 62C160 60.8954 160.895 60 162 60H298C299.105 60 300 60.8954 300 62V82C300 83.1046 299.105 84 298 84H162C160.895 84 160 83.1046 160 82V62Z"
fill="#F3F4F6" />
<path
d="M324 62C324 60.8954 324.895 60 326 60H346C347.105 60 348 60.8954 348 62V82C348 83.1046 347.105 84 346 84H326C324.895 84 324 83.1046 324 82V62Z"
fill="#F3F4F6" />
<path
d="M364 62C364 60.8954 364.895 60 366 60H466C467.105 60 468 60.8954 468 62V82C468 83.1046 467.105 84 466 84H366C364.895 84 364 83.1046 364 82V62Z"
fill="#F3F4F6" />
<path
d="M0 147C0 145.343 1.34315 144 3 144H93C94.6569 144 96 145.343 96 147V237C96 238.657 94.6569 240 93 240H3C1.34315 240 0 238.657 0 237V147Z"
fill="#F3F4F6" />
<path
d="M120 162C120 160.895 120.895 160 122 160H590C591.105 160 592 160.895 592 162V182C592 183.105 591.105 184 590 184H122C120.895 184 120 183.105 120 182V162Z"
fill="#F3F4F6" />
<path
d="M120 206C120 204.895 120.895 204 122 204H142C143.105 204 144 204.895 144 206V226C144 227.105 143.105 228 142 228H122C120.895 228 120 227.105 120 226V206Z"
fill="#F3F4F6" />
<path
d="M160 206C160 204.895 160.895 204 162 204H298C299.105 204 300 204.895 300 206V226C300 227.105 299.105 228 298 228H162C160.895 228 160 227.105 160 226V206Z"
fill="#F3F4F6" />
<path
d="M324 206C324 204.895 324.895 204 326 204H346C347.105 204 348 204.895 348 206V226C348 227.105 347.105 228 346 228H326C324.895 228 324 227.105 324 226V206Z"
fill="#F3F4F6" />
<path
d="M364 206C364 204.895 364.895 204 366 204H466C467.105 204 468 204.895 468 206V226C468 227.105 467.105 228 466 228H366C364.895 228 364 227.105 364 226V206Z"
fill="#F3F4F6" />
<path
d="M0 291C0 289.343 1.34315 288 3 288H93C94.6569 288 96 289.343 96 291V381C96 382.657 94.6569 384 93 384H3C1.34315 384 0 382.657 0 381V291Z"
fill="#F3F4F6" />
<path
d="M120 306C120 304.895 120.895 304 122 304H590C591.105 304 592 304.895 592 306V326C592 327.105 591.105 328 590 328H122C120.895 328 120 327.105 120 326V306Z"
fill="#F3F4F6" />
<path
d="M120 350C120 348.895 120.895 348 122 348H142C143.105 348 144 348.895 144 350V370C144 371.105 143.105 372 142 372H122C120.895 372 120 371.105 120 370V350Z"
fill="#F3F4F6" />
<path
d="M160 350C160 348.895 160.895 348 162 348H298C299.105 348 300 348.895 300 350V370C300 371.105 299.105 372 298 372H162C160.895 372 160 371.105 160 370V350Z"
fill="#F3F4F6" />
<path
d="M324 350C324 348.895 324.895 348 326 348H346C347.105 348 348 348.895 348 350V370C348 371.105 347.105 372 346 372H326C324.895 372 324 371.105 324 370V350Z"
fill="#F3F4F6" />
<path
d="M364 350C364 348.895 364.895 348 366 348H466C467.105 348 468 348.895 468 350V370C468 371.105 467.105 372 466 372H366C364.895 372 364 371.105 364 370V350Z"
fill="#F3F4F6" />
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -50,6 +50,7 @@ import _CubeIcon from './icons/cube.svg?component'
import _CurrencyIcon from './icons/currency.svg?component'
import _DashboardIcon from './icons/dashboard.svg?component'
import _DatabaseIcon from './icons/database.svg?component'
import _DotIcon from './icons/dot.svg?component'
import _DownloadIcon from './icons/download.svg?component'
import _DropdownIcon from './icons/dropdown.svg?component'
import _EditIcon from './icons/edit.svg?component'
@@ -243,6 +244,7 @@ export const CubeIcon = _CubeIcon
export const CurrencyIcon = _CurrencyIcon
export const DashboardIcon = _DashboardIcon
export const DatabaseIcon = _DatabaseIcon
export const DotIcon = _DotIcon
export const DownloadIcon = _DownloadIcon
export const DropdownIcon = _DropdownIcon
export const EditIcon = _EditIcon

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dot-icon lucide-dot"><circle cx="12.1" cy="12.1" r="1"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -12,6 +12,7 @@ import './omorphia.scss'
import _ModrinthIcon from './branding/logo.svg?component'
import _FourOhFourNotFound from './branding/404.svg?component'
import _ModrinthPlusIcon from './branding/modrinth-plus.svg?component'
import _ServersManageIllustration from './branding/illustrations/servers-background.svg?component'
import _AngryRinthbot from './branding/rinthbot/angry.webp'
import _AnnoyedRinthbot from './branding/rinthbot/annoyed.webp'
import _ConfusedRinthbot from './branding/rinthbot/confused.webp'
@@ -50,6 +51,7 @@ import _YouTubeIcon from './external/youtube.svg?component'
export const ModrinthIcon = _ModrinthIcon
export const FourOhFourNotFound = _FourOhFourNotFound
export const ModrinthPlusIcon = _ModrinthPlusIcon
export const ServersManageIllustration = _ServersManageIllustration
export const AngryRinthbot = _AngryRinthbot
export const AnnoyedRinthbot = _AnnoyedRinthbot
export const ConfusedRinthbot = _ConfusedRinthbot

View File

@@ -1,3 +1,4 @@
export * from './src/components'
export * from './src/utils'
export * from './src/composables'
export * from './src/servers'

View File

@@ -0,0 +1,5 @@
<template>
<div class="flex items-center gap-2 w-fit px-3 py-1 bg-button-bg rounded-full text-sm">
<slot />
</div>
</template>

View File

@@ -36,6 +36,7 @@ export { default as ProgressBar } from './base/ProgressBar.vue'
export { default as ProjectCard } from './base/ProjectCard.vue'
export { default as RadialHeader } from './base/RadialHeader.vue'
export { default as RadioButtons } from './base/RadioButtons.vue'
export { default as RaisedBadge } from './base/RaisedBadge.vue'
export { default as ScrollablePanel } from './base/ScrollablePanel.vue'
export { default as ServerNotice } from './base/ServerNotice.vue'
export { default as SimpleBadge } from './base/SimpleBadge.vue'

View File

@@ -0,0 +1,141 @@
<template>
<Card>
<div class="server-card-grid">
<div class="header-section flex gap-4 items-center mb-4">
<Avatar size="4rem" />
<div class="flex flex-col gap-2">
<span class="text-xl text-contrast font-bold">{{ server_name }}</span>
<span class="text-md text-secondary" v-tooltip="server_created.toLocaleString()">
Created {{ formatRelativeTime(server_created) }}
</span>
</div>
</div>
<div class="badges-section flex gap-2 items-center mb-4">
<RaisedBadge>{{ server_plan }}</RaisedBadge>
<RaisedBadge class="text-lg" :color="serverStatusColor">
&bull; {{ formattedServerStatus }}
</RaisedBadge>
</div>
<div class="content-section flex flex-col gap-2 mb-4">
<div class="flex flex-row gap-2">
<UsersIcon class="size-4 my-auto" />
<span class="text-secondary">
{{ players_online }} / {{ max_players_online }} players
</span>
</div>
<div class="flex flex-row gap-2">
<GlobeIcon class="size-4 my-auto" />
<span class="text-secondary">{{ world_name }}</span>
</div>
<div class="flex flex-row gap-2">
<LinkIcon class="size-4 my-auto" />
<CopyCode :text="ip" />
</div>
</div>
<div class="actions-section flex gap-2">
<ButtonStyled color="brand">
<RouterLink :to="`/servers/manage/${id}`">
<EditIcon class="size-4" />
Manage
</RouterLink>
</ButtonStyled>
<ButtonStyled>
<RouterLink :to="`/servers/manage/${id}`">
<CurrencyIcon class="size-4" />
Billing
</RouterLink>
</ButtonStyled>
</div>
</div>
</Card>
</template>
<script setup lang="ts">
import { CurrencyIcon, EditIcon, GlobeIcon, LinkIcon, UsersIcon } from '@modrinth/assets'
import { Avatar, Card, RaisedBadge, useRelativeTime, CopyCode, ButtonStyled } from '@modrinth/ui'
import { computed } from 'vue'
import { RouterLink } from 'vue-router'
const props = defineProps<{
server_name: string
server_created: Date
server_plan: string
server_status: string
players_online: number
max_players_online: number
world_name: string
ip: string
id: string
}>()
const formatRelativeTime = useRelativeTime()
const serverStatusColor = computed(() => {
switch (props.server_status) {
case 'online':
return 'green'
case 'restarting':
return 'orange'
case 'offline':
return undefined
default:
return undefined
}
})
const formattedServerStatus = computed(() => {
return props.server_status.slice(0, 1).toUpperCase() + props.server_status.slice(1)
})
</script>
<style scoped>
.server-card-grid {
display: grid;
grid-template-areas:
'header badges'
'content content'
'actions actions';
grid-template-columns: 1fr auto;
align-items: start;
}
@media (max-width: 768px) {
.server-card-grid {
grid-template-areas:
'header'
'badges'
'content'
'actions';
grid-template-columns: 1fr;
}
.badges-section {
justify-self: start;
}
}
@media (min-width: 769px) {
.badges-section {
justify-self: end;
}
}
.header-section {
grid-area: header;
}
.badges-section {
grid-area: badges;
}
.content-section {
grid-area: content;
}
.actions-section {
grid-area: actions;
}
</style>

View File

@@ -0,0 +1 @@
export { default as ServersManagePage } from './pages/manage.vue'

View File

View File

@@ -0,0 +1,130 @@
<template>
<div v-if="servers.length + sharedServers.length === 0" class="text-center py-24 relative">
<ServersManageIllustration class="servers-manage-illustration" />
<ServerIcon class="size-24 mx-auto text-contrast mb-4" />
<h3 class="text-3xl font-medium text-contrast mb-2">No servers found</h3>
<p class="text-gray-500 mb-6 px-4">Get started by creating your first server</p>
<ButtonStyled color="green" size="large" type="outlined">
<button class="flex items-center justify-center gap-2 mx-auto">
<PlusIcon class="size-4" /> New server
</button>
</ButtonStyled>
</div>
<div v-else class="flex flex-col sm:flex-row gap-4 sm:gap-0 my-4">
<div class="flex flex-col gap-2">
<span class="text-3xl text-contrast font-bold">Servers</span>
<span class="text-sm sm:text-base text-secondary">View and manage all your servers</span>
</div>
<div class="sm:ml-auto">
<ButtonStyled color="green" size="large" class="w-full sm:w-auto">
<button class="flex items-center justify-center gap-2">
<PlusIcon class="size-4" />
<span>New server</span>
</button>
</ButtonStyled>
</div>
</div>
<template v-if="servers.length > 0">
<span class="text-xl text-contrast font-bold mb-4 flex flex-row gap-2"> Your servers </span>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 gap-y-2">
<ServerCard
v-for="server in servers"
:id="server.id"
:key="server.id"
:server_name="server.server_name"
:server_created="server.server_created"
:server_plan="server.server_plan"
:server_status="server.server_status"
:players_online="server.players_online"
:max_players_online="server.max_players_online"
:world_name="server.world_name"
:ip="server.ip"
/>
</div>
</template>
<template v-if="sharedServers.length > 0">
<span class="text-xl text-contrast font-bold mb-4 flex flex-row gap-2"> Shared servers </span>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 gap-y-2">
<ServerCard
v-for="server in sharedServers"
:id="server.id"
:key="server.id"
:server_name="server.server_name"
:server_created="server.server_created"
:server_plan="server.server_plan"
:server_status="server.server_status"
:players_online="server.players_online"
:max_players_online="server.max_players_online"
:world_name="server.world_name"
:ip="server.ip"
/>
</div>
</template>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ButtonStyled } from '@modrinth/ui'
import ServerCard from '../components/management/ServerCard.vue'
import { PlusIcon, ServerIcon, ServersManageIllustration } from '@modrinth/assets'
const sharedServers = ref([
{
id: 'server-1',
server_name: 'Rinth SMP',
server_created: new Date('2023-10-01T12:00:00Z'),
server_plan: 'Large',
server_status: 'online',
players_online: 5,
max_players_online: 20,
world_name: 'Example World',
ip: 'valiant-apple.modrinth.gg',
},
])
// const sharedServers = ref([])
// const servers = ref([])
const servers = ref([
{
id: 'server-1',
server_name: 'Rinth SMP',
server_created: new Date('2023-10-01T12:00:00Z'),
server_plan: 'Large',
server_status: 'online',
players_online: 5,
max_players_online: 20,
world_name: 'Example World',
ip: 'valiant-apple.modrinth.gg',
},
{
id: 'server-1',
server_name: 'Rinth SMP',
server_created: new Date('2023-10-01T12:00:00Z'),
server_plan: 'Large',
server_status: 'online',
players_online: 5,
max_players_online: 20,
world_name: 'Example World',
ip: 'valiant-apple.modrinth.gg',
},
])
</script>
<style scoped lang="scss">
.servers-manage-illustration {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: -1;
width: 500px;
height: 500px;
opacity: 0.05;
mask: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
-webkit-mask: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
}
</style>