923 lines
No EOL
29 KiB
Svelte
923 lines
No EOL
29 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from "svelte";
|
|
//@ts-ignore
|
|
import { TAClient, Response_ResponseType, Map, Tournament, GameplayModifiers_GameOptions } from 'moons-ta-client';
|
|
import { TABotTokenStore, TAServerPort, TAServerUrl, authTokenStore } from "$lib/stores";
|
|
import { bufferToImageUrl } from "$lib/services/taImages.js";
|
|
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
|
|
import Popup from "$lib/components/notifications/Popup.svelte";
|
|
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
|
|
import EditMapModal from "$lib/components/popups/EditMap.svelte";
|
|
import { goto } from "$app/navigation";
|
|
|
|
export let data;
|
|
|
|
const tournamentGuid: string = data.tournamentGuid;
|
|
const poolGuid: string = data.poolGuid;
|
|
|
|
let tournament: Tournament | undefined;
|
|
let isLoading = false;
|
|
let error: string | null = null;
|
|
let poolName: string = "";
|
|
let maps: {
|
|
taData: Map,
|
|
beatsaverData: BeatSaverMap
|
|
}[] = [];
|
|
|
|
type BeatSaverMap = {
|
|
id: string;
|
|
metadata: {
|
|
songName: string;
|
|
songAuthorName: string;
|
|
levelAuthorName: string;
|
|
songSubName: string;
|
|
duration: number;
|
|
bpm: number;
|
|
};
|
|
versions: Array<{
|
|
hash: string;
|
|
state: string;
|
|
createdAt: string;
|
|
downloadURL: string;
|
|
diffs: Array<{
|
|
characteristic: string;
|
|
difficulty: string;
|
|
maxScore: number;
|
|
nps: number;
|
|
}>;
|
|
coverURL: string;
|
|
previewURL: string;
|
|
}>;
|
|
uploaded: string;
|
|
};
|
|
|
|
enum MapDifficulty {
|
|
"Easy" = 0,
|
|
"Normal" = 1,
|
|
"Hard" = 2,
|
|
"Expert" = 3,
|
|
"Expert+" = 4
|
|
}
|
|
|
|
interface Modifier {
|
|
id: GameplayModifiers_GameOptions;
|
|
name: string;
|
|
description: string;
|
|
enabled: boolean;
|
|
disabled?: boolean;
|
|
group?: string;
|
|
incompatibilites?: GameplayModifiers_GameOptions[];
|
|
}
|
|
|
|
let gameModifiers: Modifier[] = [
|
|
// Player modifiers
|
|
{ id: GameplayModifiers_GameOptions.NoFail, name: "No Fail", description: "You can't fail the level", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.InstaFail, GameplayModifiers_GameOptions.BatteryEnergy] },
|
|
{ id: GameplayModifiers_GameOptions.InstaFail, name: "One Life", description: "You only have one life", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoFail, GameplayModifiers_GameOptions.BatteryEnergy] },
|
|
{ id: GameplayModifiers_GameOptions.BatteryEnergy, name: "Four Lives", description: "You have four lives", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoFail, GameplayModifiers_GameOptions.InstaFail] },
|
|
{ id: GameplayModifiers_GameOptions.NoBombs, name: "No Bombs", description: "No bombs will appear", enabled: false, group: "Player" },
|
|
{ id: GameplayModifiers_GameOptions.NoObstacles, name: "No Walls", description: "No walls will appear", enabled: false, group: "Player" },
|
|
{ id: GameplayModifiers_GameOptions.NoArrows, name: "No Arrows", description: "All notes can be cut in any direction", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.DisappearingArrows] },
|
|
{ id: GameplayModifiers_GameOptions.GhostNotes, name: "Ghost Notes", description: "Note colors are hidden", enabled: false, group: "Player" },
|
|
{ id: GameplayModifiers_GameOptions.DisappearingArrows, name: "Disappearing Arrows", description: "Arrows disappear as they approach you", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoArrows] },
|
|
|
|
// Speed modifiers
|
|
{ id: GameplayModifiers_GameOptions.SlowSong, name: "Slower Song", description: "Reduces the song speed by 15%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.FastSong, GameplayModifiers_GameOptions.SuperFastSong] },
|
|
{ id: GameplayModifiers_GameOptions.FastSong, name: "Faster Song", description: "Increases the song speed by 20%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.SlowSong, GameplayModifiers_GameOptions.SuperFastSong] },
|
|
{ id: GameplayModifiers_GameOptions.SuperFastSong, name: "Super Fast Song", description: "Increases the song speed by 50%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.SlowSong, GameplayModifiers_GameOptions.FastSong] },
|
|
|
|
// Environment modifiers
|
|
{ id: GameplayModifiers_GameOptions.SmallCubes, name: "Small Notes", description: "Notes are smaller", enabled: false, group: "Environment", incompatibilites: [GameplayModifiers_GameOptions.ProMode] },
|
|
{ id: GameplayModifiers_GameOptions.ProMode, name: "Pro Mode", description: "Makes notes smaller, removes debris, and adds hit scores", enabled: false, group: "Environment", incompatibilites: [GameplayModifiers_GameOptions.SmallCubes] },
|
|
{ id: GameplayModifiers_GameOptions.ZenMode, name: "Zen Mode", description: "No fail, no bombs, reduced obstacles", enabled: false, group: "Environment" },
|
|
{ id: GameplayModifiers_GameOptions.StrictAngles, name: "Strict Angles", description: "Stricter angle enforcement for cuts", enabled: false, group: "Environment" }
|
|
];
|
|
|
|
// Edit Map Modal state
|
|
let showEditMapModal: boolean = false;
|
|
let currentEditMap: Map | null = null;
|
|
|
|
// Delete confirmation popup
|
|
let showDeleteConfirmation = false;
|
|
let mapToDelete: string | null = null;
|
|
|
|
// Info popup state
|
|
let showInfoPopup: boolean = false;
|
|
let infoPopupContent: string = "";
|
|
|
|
// Success notification
|
|
let showSuccessNotification = false;
|
|
let successMessage = "";
|
|
|
|
const client = new TAClient();
|
|
|
|
// Remove 'Bearer ' from the token
|
|
if ($authTokenStore) {
|
|
const cleanToken = $authTokenStore.replace('Bearer ', '');
|
|
client.setAuthToken(cleanToken);
|
|
}
|
|
|
|
function openInfoPopup(content: string) {
|
|
infoPopupContent = content;
|
|
showInfoPopup = true;
|
|
}
|
|
|
|
function closeInfoPopup() {
|
|
showInfoPopup = false;
|
|
}
|
|
|
|
function openEditMapModal(map: Map | null = null) {
|
|
currentEditMap = map;
|
|
showEditMapModal = true;
|
|
}
|
|
|
|
function closeEditMapModal() {
|
|
showEditMapModal = false;
|
|
currentEditMap = null;
|
|
}
|
|
|
|
function openDeleteConfirmation(mapId: string) {
|
|
mapToDelete = mapId;
|
|
showDeleteConfirmation = true;
|
|
}
|
|
|
|
function closeDeleteConfirmation() {
|
|
showDeleteConfirmation = false;
|
|
mapToDelete = null;
|
|
}
|
|
|
|
function closeNotification() {
|
|
showSuccessNotification = false;
|
|
}
|
|
|
|
function handleMapUpdated(event: CustomEvent) {
|
|
fetchMapPoolData();
|
|
showSuccessNotification = true;
|
|
successMessage = "Map successfully updated!";
|
|
}
|
|
|
|
function handleMapAdded(event: CustomEvent) {
|
|
fetchMapPoolData();
|
|
showSuccessNotification = true;
|
|
successMessage = "Map successfully added!";
|
|
}
|
|
|
|
async function deleteMap() {
|
|
if (!mapToDelete) return;
|
|
|
|
isLoading = true;
|
|
|
|
try {
|
|
// Implement actual map deletion - this will depend on your API
|
|
const removeMapResponse = await client.removeTournamentPoolMap(tournamentGuid, poolGuid, mapToDelete);
|
|
|
|
if (removeMapResponse.type !== Response_ResponseType.Success) {
|
|
throw new Error("Failed to remove map! Please refresh this page!");
|
|
}
|
|
|
|
maps = maps.filter(map => map.taData.guid !== mapToDelete);
|
|
showSuccessNotification = true;
|
|
successMessage = "Map successfully deleted!";
|
|
} catch (err) {
|
|
console.error('Error deleting map:', err);
|
|
error = err instanceof Error ? err.message : 'An unknown error occurred';
|
|
} finally {
|
|
isLoading = false;
|
|
closeDeleteConfirmation();
|
|
}
|
|
}
|
|
|
|
function goBackToMapPools() {
|
|
goto(`/tournaments/${tournamentGuid}/mappools`);
|
|
}
|
|
|
|
async function fetchMapPoolData() {
|
|
if (!$authTokenStore) {
|
|
window.location.href = '/discordAuth';
|
|
return;
|
|
}
|
|
|
|
isLoading = true;
|
|
error = null;
|
|
|
|
try {
|
|
if(!client.isConnected) {
|
|
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
|
|
|
|
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
|
|
throw new Error(connectResult.details.connect.message);
|
|
}
|
|
|
|
const joinResult = await client.joinTournament(tournamentGuid);
|
|
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
|
|
throw new Error('Could not join tournament');
|
|
}
|
|
|
|
tournament = await joinResult.details.join.state?.tournaments.find((x: Tournament) => x.guid === tournamentGuid);
|
|
} else {
|
|
tournament = await client.stateManager.getTournament(tournamentGuid)!;
|
|
}
|
|
|
|
|
|
// Find the map pool with the specified GUID
|
|
const pool = await tournament?.settings?.pools.find(p => p.guid === poolGuid);
|
|
|
|
if (!pool) {
|
|
throw new Error('Map pool not found');
|
|
}
|
|
|
|
poolName = pool.name;
|
|
await pool.maps.map(async(map) => {
|
|
console.log(map);
|
|
let beatsaverData = await getMapBeatsaverData(map.gameplayParameters!.beatmap!.levelId);
|
|
maps = [...maps, {
|
|
taData: map,
|
|
beatsaverData: beatsaverData
|
|
}];
|
|
});
|
|
|
|
maps = maps;
|
|
} catch (err) {
|
|
console.error('Error fetching map pool data:', err);
|
|
error = err instanceof Error ? err.message : 'An unknown error occurred';
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
async function getMapBeatsaverData(levelId: string): Promise<BeatSaverMap> {
|
|
const newLevelId = levelId.replace("custom_level_", "");
|
|
|
|
const data = await fetch(`https://api.beatsaver.com/maps/hash/${newLevelId}`);
|
|
|
|
if(!data.ok) {
|
|
error = "Unable to fetch a map-s BeatSaver information. Please go to inspect, console, and take a screenshot and send it to serverbp or matrikmoon on Discord."
|
|
}
|
|
|
|
const response: BeatSaverMap = await data.json();
|
|
|
|
return response;
|
|
}
|
|
|
|
onMount(async() => {
|
|
if ($authTokenStore) {
|
|
await fetchMapPoolData();
|
|
} else {
|
|
window.location.href = "/discordAuth";
|
|
}
|
|
});
|
|
|
|
onDestroy(() => {
|
|
client.disconnect();
|
|
});
|
|
</script>
|
|
|
|
<div class="layout">
|
|
<main class="dashboard">
|
|
<SideMenu currentPage="mappools" tournamentName={tournament?.settings?.tournamentName} tournamnentGuid={tournamentGuid} />
|
|
|
|
<div class="content-area">
|
|
<div class="content-header">
|
|
<div class="header-left">
|
|
<button class="back-button" on:click={goBackToMapPools}>
|
|
<span class="material-icons">arrow_back</span>
|
|
Back to Map Pools
|
|
</button>
|
|
<h2>{poolName} Maps</h2>
|
|
</div>
|
|
<div class="actions">
|
|
{#if !error && !isLoading}
|
|
<button class="action-button add" on:click={() => openEditMapModal()}>
|
|
<span class="material-icons">add</span>
|
|
Add Map
|
|
</button>
|
|
<button class="action-button refresh" on:click={fetchMapPoolData}>
|
|
<span class="material-icons">refresh</span>
|
|
Refresh
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="maps-header">
|
|
<h3>Maps in this Pool</h3>
|
|
<button class="info-button" on:click={() => openInfoPopup("Maps are Beat Saber songs that can be played in your tournament. Each map can have different modifiers and settings configured.")}>
|
|
<span class="material-icons">info</span>
|
|
</button>
|
|
</div>
|
|
|
|
{#if isLoading}
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<p>Loading maps data...</p>
|
|
</div>
|
|
{:else if error}
|
|
<div class="error-container">
|
|
<span class="material-icons">error_outline</span>
|
|
<p>{error}</p>
|
|
<button class="retry-button" on:click={fetchMapPoolData}>Retry</button>
|
|
</div>
|
|
{:else if maps.length === 0}
|
|
<div class="no-maps">
|
|
<span class="material-icons">music_note</span>
|
|
<p>No maps have been added to this pool yet</p>
|
|
<button class="action-button add" on:click={() => openEditMapModal()}>
|
|
<span class="material-icons">add</span>
|
|
Add First Map
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<div class="maps-list">
|
|
{#each maps as map}
|
|
<div class="map-card">
|
|
<div class="map-content">
|
|
<div class="map-image">
|
|
<img
|
|
src={map.beatsaverData.versions[0].coverURL}
|
|
alt="{map.taData.gameplayParameters?.beatmap?.name} cover"
|
|
/>
|
|
</div>
|
|
<div class="map-info">
|
|
<h4>{map.taData.gameplayParameters?.beatmap?.name}</h4>
|
|
<p class="map-author">By {map.beatsaverData.metadata.levelAuthorName || "Unknown Artist"}</p>
|
|
<div class="map-details">
|
|
{#if map.taData.gameplayParameters?.beatmap?.difficulty}
|
|
<span class="map-difficulty">
|
|
<span class="material-icons">speed</span>
|
|
{MapDifficulty[map.taData.gameplayParameters?.beatmap?.difficulty]}
|
|
</span>
|
|
{/if}
|
|
<!-- {#if map.length}
|
|
<span class="map-length">
|
|
<span class="material-icons">access_time</span>
|
|
{map.length}
|
|
</span>
|
|
{/if} -->
|
|
</div>
|
|
</div>
|
|
<div class="map-actions">
|
|
<button
|
|
class="map-action edit"
|
|
title="Edit map"
|
|
on:click={() => openEditMapModal(map.taData)}
|
|
>
|
|
<span class="material-icons">edit</span>
|
|
</button>
|
|
<button
|
|
class="map-action delete"
|
|
title="Delete map"
|
|
on:click={() => openDeleteConfirmation(map.taData.guid)}
|
|
>
|
|
<span class="material-icons">delete</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="map-modifiers">
|
|
<h5>Active Modifiers</h5>
|
|
<div class="modifier-tags">
|
|
<!-- {#if map.modifiers && map.modifiers.length > 0}
|
|
{#each map.modifiers as modifier}
|
|
<span class="modifier-tag">{modifier}</span>
|
|
{/each}
|
|
{:else}
|
|
<span class="no-modifiers">No modifiers active</span>
|
|
{/if} -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Edit Map Modal Component -->
|
|
{#if showEditMapModal}
|
|
<EditMapModal
|
|
tournamentGuid={tournamentGuid}
|
|
poolGuid={poolGuid}
|
|
map={maps.find(map => map.taData.guid == currentEditMap?.guid)}
|
|
on:close={closeEditMapModal}
|
|
on:mapUpdated={handleMapUpdated}
|
|
on:mapAdded={handleMapAdded}
|
|
/>
|
|
{/if}
|
|
|
|
<!-- Info Popup -->
|
|
{#if showInfoPopup}
|
|
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
|
|
{/if}
|
|
|
|
<!-- Delete Confirmation Popup -->
|
|
{#if showDeleteConfirmation}
|
|
<div class="popup-overlay">
|
|
<div class="popup-container delete-confirmation">
|
|
<div class="popup-header">
|
|
<h3>Delete Map</h3>
|
|
<button class="close-button" on:click={closeDeleteConfirmation}>
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="popup-content">
|
|
<p>Are you sure you want to delete this map?</p>
|
|
<p class="warning">This action cannot be undone!</p>
|
|
|
|
<div class="popup-actions">
|
|
<button class="action-button cancel" on:click={closeDeleteConfirmation}>Cancel</button>
|
|
<button class="action-button delete" on:click={deleteMap}>
|
|
Delete Map
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Success Notification -->
|
|
<Popup
|
|
bind:open={showSuccessNotification}
|
|
message={successMessage}
|
|
icon="check"
|
|
iconColor="success"
|
|
buttons={[]}
|
|
on:close={closeNotification}
|
|
/>
|
|
|
|
<style>
|
|
/* Main layout */
|
|
.layout {
|
|
display: flex;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.dashboard {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.content-area {
|
|
flex: 1;
|
|
padding: 2rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Content header */
|
|
.content-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.back-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: none;
|
|
border: none;
|
|
color: var(--accent-color);
|
|
font-weight: 500;
|
|
padding: 0.5rem 0;
|
|
cursor: pointer;
|
|
transition: color 0.2s;
|
|
margin-right: 2rem;
|
|
}
|
|
|
|
.back-button:hover {
|
|
color: var(--accent-hover);
|
|
}
|
|
|
|
.content-header h2 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.action-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background-color: var(--bg-secondary);
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
padding: 0.5rem 1rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.action-button.add,
|
|
.action-button.create {
|
|
background-color: var(--accent-color);
|
|
color: white;
|
|
}
|
|
|
|
.action-button.add:hover,
|
|
.action-button.create:hover {
|
|
background-color: var(--accent-hover);
|
|
}
|
|
|
|
.action-button.cancel {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.action-button.cancel:hover {
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.action-button.delete {
|
|
background-color: #ff5555;
|
|
color: white;
|
|
}
|
|
|
|
.action-button.delete:hover {
|
|
background-color: #ff3333;
|
|
}
|
|
|
|
.action-button.refresh:hover {
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.action-button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Maps header with info button */
|
|
.maps-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.maps-header h3 {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
margin: 0;
|
|
}
|
|
|
|
.info-button {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--accent-color);
|
|
padding: 0;
|
|
}
|
|
|
|
.info-button .material-icons {
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
.info-button:hover {
|
|
color: var(--accent-hover);
|
|
}
|
|
|
|
/* Maps list */
|
|
.maps-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.map-card {
|
|
background-color: var(--bg-secondary);
|
|
border-radius: 0.5rem;
|
|
padding: 1.25rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.map-content {
|
|
display: flex;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.map-image {
|
|
flex-shrink: 0;
|
|
width: 6rem;
|
|
height: 6rem;
|
|
}
|
|
|
|
.map-image img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 0.375rem;
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.map-info {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.map-info h4 {
|
|
margin: 0 0 0.25rem 0;
|
|
font-size: 1.125rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.map-author {
|
|
color: var(--text-secondary);
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.map-details {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.map-difficulty,
|
|
.map-length {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
color: var(--text-secondary);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.map-difficulty .material-icons,
|
|
.map-length .material-icons {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.map-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.map-action {
|
|
background: none;
|
|
border: none;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
border-radius: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.map-action:hover {
|
|
background-color: var(--bg-tertiary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.map-action.edit:hover {
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
.map-action.delete:hover {
|
|
color: #ff5555;
|
|
}
|
|
|
|
.map-modifiers {
|
|
margin-top: 1rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--bg-tertiary);
|
|
}
|
|
|
|
.map-modifiers h5 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.modifier-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.modifier-tag {
|
|
background-color: var(--bg-tertiary);
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 1rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.no-modifiers {
|
|
color: var(--text-secondary);
|
|
font-size: 0.875rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* No maps state */
|
|
.no-maps {
|
|
background-color: var(--bg-secondary);
|
|
border-radius: 0.75rem;
|
|
padding: 3rem 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.no-maps .material-icons {
|
|
font-size: 3rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.no-maps p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* Loading state */
|
|
.loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 3rem 0;
|
|
}
|
|
|
|
.spinner {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border: 0.25rem solid rgba(var(--accent-color-rgb), 0.2);
|
|
border-radius: 50%;
|
|
border-top-color: var(--accent-color);
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Error state */
|
|
.error-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 3rem 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.error-container .material-icons {
|
|
font-size: 3rem;
|
|
color: #ff5555;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.retry-button {
|
|
background-color: var(--accent-color);
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
color: white;
|
|
font-weight: 500;
|
|
padding: 0.5rem 1rem;
|
|
margin-top: 1rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.retry-button:hover {
|
|
background-color: var(--accent-hover);
|
|
}
|
|
|
|
/* Delete confirmation popup */
|
|
.popup-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.popup-container {
|
|
background-color: var(--bg-primary);
|
|
border-radius: 0.75rem;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.popup-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1.25rem 1.5rem;
|
|
border-bottom: 1px solid var(--bg-secondary);
|
|
}
|
|
|
|
.popup-header h3 {
|
|
margin: 0;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.close-button {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--text-secondary);
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.close-button:hover {
|
|
background-color: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.popup-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.popup-content p {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.popup-content .warning {
|
|
color: #ff5555;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.popup-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.75rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
/* Responsive design */
|
|
@media (max-width: 768px) {
|
|
.content-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.header-left {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.actions {
|
|
width: 100%;
|
|
}
|
|
|
|
.action-button {
|
|
flex: 1;
|
|
justify-content: center;
|
|
}
|
|
|
|
.map-content {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.map-image {
|
|
width: 100%;
|
|
height: auto;
|
|
aspect-ratio: 1;
|
|
max-width: 10rem;
|
|
margin: 0 auto 1rem;
|
|
}
|
|
|
|
.map-actions {
|
|
margin-left: 0;
|
|
margin-top: 1rem;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style> |