generated from VLADIMIR/template_frontend
update design
This commit is contained in:
+21
-2
@@ -1,10 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Родники</title>
|
<meta name="description" content="Расписание выступлений коллектива «Родники»">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<script>
|
||||||
|
// Ставим тему до первой отрисовки, чтобы не было «мигания»
|
||||||
|
(function () {
|
||||||
|
var t = null;
|
||||||
|
try {
|
||||||
|
t = localStorage.getItem('rodniki-theme');
|
||||||
|
} catch (e) {}
|
||||||
|
if (t !== 'light' && t !== 'dark') {
|
||||||
|
t =
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<title>Родники — расписание</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "vo1",
|
"name": "${REPO_NAME_SNAKE}",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vo1",
|
"name": "${REPO_NAME_SNAKE}",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
|
|||||||
+1
-10
@@ -1,16 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SchedulePage from './components/SchedulePage.vue';
|
import SchedulePage from './components/SchedulePage.vue';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<SchedulePage />
|
||||||
<SchedulePage></SchedulePage>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app {
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+184
-63
@@ -1,88 +1,209 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
/* ============================================================
|
||||||
|
Дизайн-токены «Родники»
|
||||||
|
Палитра: белый + голубой + синий.
|
||||||
|
Тёмная тема включается атрибутом data-theme="dark" на <html>
|
||||||
|
(см. composables/useTheme.ts).
|
||||||
|
============================================================ */
|
||||||
:root {
|
:root {
|
||||||
--vt-c-white: #ffffff;
|
color-scheme: light;
|
||||||
--vt-c-white-soft: #f8f8f8;
|
|
||||||
--vt-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--vt-c-black: #181818;
|
/* Фон и поверхности — белый, с едва заметным голубым отливом */
|
||||||
--vt-c-black-soft: #222222;
|
--c-canvas-1: #ffffff;
|
||||||
--vt-c-black-mute: #282828;
|
--c-canvas-2: #eef5fd;
|
||||||
|
--c-surface: #ffffff;
|
||||||
|
--c-surface-soft: #f6faff;
|
||||||
|
|
||||||
--vt-c-indigo: #2c3e50;
|
/* Текст */
|
||||||
|
--c-ink: #1b2c47;
|
||||||
|
--c-ink-soft: #51688a;
|
||||||
|
--c-ink-faint: #5a7194;
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
/* Акцент — синий */
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
--c-accent: #2563eb;
|
||||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
--c-accent-deep: #1e4fd6;
|
||||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
--c-accent-soft: #dbeafe;
|
||||||
|
--c-accent-text: #1e40af;
|
||||||
|
|
||||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
/* Вторичный акцент — голубой */
|
||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
--c-sky: #4f9cf7;
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
--c-sky-soft: #e6f2ff;
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
--c-sky-text: #2563eb;
|
||||||
|
|
||||||
|
/* Дополнительные краски */
|
||||||
|
--c-violet: #7c3aed;
|
||||||
|
--c-violet-soft: #ece4fe;
|
||||||
|
--c-violet-text: #6d28d9;
|
||||||
|
--c-pink-soft: #fde3ef;
|
||||||
|
--c-pink-text: #be185d;
|
||||||
|
--c-amber: #b45309;
|
||||||
|
--c-amber-soft: #fdf0d7;
|
||||||
|
--c-amber-text: #92400e;
|
||||||
|
|
||||||
|
/* Бейдж «сегодня» */
|
||||||
|
--c-today-text: #ffffff;
|
||||||
|
--c-today-glow: rgba(37, 99, 235, 0.45);
|
||||||
|
|
||||||
|
/* Линии */
|
||||||
|
--c-line: rgba(27, 44, 71, 0.12);
|
||||||
|
--c-line-strong: rgba(27, 44, 71, 0.2);
|
||||||
|
|
||||||
|
/* Форма и тени */
|
||||||
|
--r-lg: 22px;
|
||||||
|
--r-md: 14px;
|
||||||
|
--r-sm: 10px;
|
||||||
|
--r-pill: 999px;
|
||||||
|
--shadow-card: 0 1px 2px rgba(30, 64, 175, 0.06),
|
||||||
|
0 18px 40px -22px rgba(30, 64, 175, 0.35);
|
||||||
|
|
||||||
|
/* Свечение фона */
|
||||||
|
--c-glow: rgba(37, 99, 235, 0.06);
|
||||||
|
--c-glow-2: rgba(124, 58, 237, 0.07);
|
||||||
|
|
||||||
|
/* Типографика */
|
||||||
|
--font-display: 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia,
|
||||||
|
'Times New Roman', serif;
|
||||||
|
--font-body: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
:root[data-theme='dark'] {
|
||||||
:root {
|
color-scheme: dark;
|
||||||
--color-background: var(--vt-c-white);
|
|
||||||
--color-background-soft: var(--vt-c-white-soft);
|
|
||||||
--color-background-mute: var(--vt-c-white-mute);
|
|
||||||
|
|
||||||
--color-border: var(--vt-c-divider-light-2);
|
--c-canvas-1: #101a2c;
|
||||||
--color-border-hover: var(--vt-c-divider-light-1);
|
--c-canvas-2: #0b1220;
|
||||||
|
--c-surface: #16233c;
|
||||||
|
--c-surface-soft: #1d2d4b;
|
||||||
|
|
||||||
--color-heading: var(--vt-c-text-light-1);
|
--c-ink: #f1f6fc;
|
||||||
--color-text: var(--vt-c-text-light-1);
|
--c-ink-soft: #b9c9de;
|
||||||
|
--c-ink-faint: #7f93b4;
|
||||||
|
|
||||||
--section-gap: 160px;
|
--c-accent: #6ea2ff;
|
||||||
}
|
--c-accent-deep: #93b8ff;
|
||||||
|
--c-accent-soft: rgba(110, 162, 255, 0.16);
|
||||||
@media (prefers-color-scheme: dark) {
|
--c-accent-text: #a7c4ff;
|
||||||
:root {
|
|
||||||
color-scheme: only dark;
|
--c-sky: #8cc3ff;
|
||||||
|
--c-sky-soft: rgba(140, 195, 255, 0.14);
|
||||||
--color-background: var(--vt-c-black);
|
--c-sky-text: #b7d6ff;
|
||||||
--color-background-soft: var(--vt-c-black-soft);
|
|
||||||
--color-background-mute: var(--vt-c-black-mute);
|
--c-violet: #a78bfa;
|
||||||
|
--c-violet-soft: rgba(167, 139, 250, 0.16);
|
||||||
--color-border: var(--vt-c-divider-dark-2);
|
--c-violet-text: #c4b5fd;
|
||||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
--c-pink-soft: rgba(244, 114, 182, 0.16);
|
||||||
|
--c-pink-text: #f9a8d4;
|
||||||
--color-heading: var(--vt-c-text-dark-1);
|
--c-amber: #fbbf24;
|
||||||
--color-text: var(--vt-c-text-dark-2);
|
--c-amber-soft: rgba(251, 191, 36, 0.16);
|
||||||
}
|
--c-amber-text: #fcd34d;
|
||||||
|
|
||||||
|
--c-today-text: #0b1220;
|
||||||
|
--c-today-glow: rgba(110, 162, 255, 0.35);
|
||||||
|
|
||||||
|
--c-line: rgba(241, 246, 252, 0.1);
|
||||||
|
--c-line-strong: rgba(241, 246, 252, 0.2);
|
||||||
|
|
||||||
|
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3),
|
||||||
|
0 18px 40px -22px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
--c-glow: rgba(110, 162, 255, 0.08);
|
||||||
|
--c-glow-2: rgba(167, 139, 250, 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Базовый сброс ---------- */
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--color-text);
|
font-family: var(--font-body);
|
||||||
background: #ccc;
|
font-size: 16px;
|
||||||
transition:
|
line-height: 1.55;
|
||||||
color 0.5s,
|
color: var(--c-ink);
|
||||||
background-color 0.5s;
|
background: linear-gradient(180deg, var(--c-canvas-1), var(--c-canvas-2));
|
||||||
line-height: 1.6;
|
background: radial-gradient(
|
||||||
font-family:
|
900px 420px at 50% -8%,
|
||||||
Inter,
|
var(--c-glow),
|
||||||
-apple-system,
|
transparent 65%
|
||||||
BlinkMacSystemFont,
|
),
|
||||||
'Segoe UI',
|
radial-gradient(
|
||||||
Roboto,
|
620px 320px at 100% -18%,
|
||||||
Oxygen,
|
var(--c-glow-2),
|
||||||
Ubuntu,
|
transparent 60%
|
||||||
Cantarell,
|
),
|
||||||
'Fira Sans',
|
linear-gradient(180deg, var(--c-canvas-1), var(--c-canvas-2));
|
||||||
'Droid Sans',
|
background-attachment: fixed;
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
transition: color 0.3s ease, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--c-accent-soft);
|
||||||
|
color: var(--c-accent-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Утилиты ---------- */
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-29
@@ -1,35 +1,10 @@
|
|||||||
@import './base.css';
|
@import './base.css';
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
max-width: 1280px;
|
max-width: 720px;
|
||||||
|
min-height: 100vh;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: clamp(20px, 5vw, 44px) clamp(14px, 4vw, 24px) 64px;
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
.green {
|
|
||||||
text-decoration: none;
|
|
||||||
color: hsla(160, 100%, 37%, 1);
|
|
||||||
transition: 0.4s;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
a:hover {
|
|
||||||
background-color: hsla(160, 100%, 37%, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+760
-70
@@ -1,98 +1,788 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { apiGetDays } from './client';
|
import { apiGetDays } from './client';
|
||||||
import type { Schedule } from './models';
|
import type { Performance, Schedule } from './models';
|
||||||
import { formatRussianDate, cleanTime, getRelativeDayName, timeAgo, formatTimeRange } from './date';
|
import {
|
||||||
|
formatRussianDate,
|
||||||
|
cleanTime,
|
||||||
|
getRelativeDayName,
|
||||||
|
timeAgo,
|
||||||
|
formatTimeRange,
|
||||||
|
} from './date';
|
||||||
import { capitalizeFirstLetter } from './text';
|
import { capitalizeFirstLetter } from './text';
|
||||||
|
import { useTheme } from '../composables/useTheme';
|
||||||
|
import IconClock from './icons/IconClock.vue';
|
||||||
|
import IconPin from './icons/IconPin.vue';
|
||||||
|
import IconMusic from './icons/IconMusic.vue';
|
||||||
|
import IconSparkles from './icons/IconSparkles.vue';
|
||||||
|
import IconRefresh from './icons/IconRefresh.vue';
|
||||||
|
import IconSun from './icons/IconSun.vue';
|
||||||
|
import IconMoon from './icons/IconMoon.vue';
|
||||||
|
|
||||||
const schedule = ref<Schedule>({ updateTime: "", days: [] })
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
onMounted(async () => {
|
const themeLabel = computed(() =>
|
||||||
schedule.value = await apiGetDays()
|
theme.value === 'dark' ? 'Включить светлую тему' : 'Включить тёмную тему',
|
||||||
})
|
);
|
||||||
|
|
||||||
|
const schedule = ref<Schedule>({ updateTime: '', days: [] });
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(false);
|
||||||
|
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
schedule.value = await apiGetDays();
|
||||||
|
error.value = false;
|
||||||
|
} catch {
|
||||||
|
// При неудачном обновлении показываем ошибку только если данных ещё нет
|
||||||
|
if (schedule.value.days.length === 0) {
|
||||||
|
error.value = true;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(load);
|
||||||
|
|
||||||
|
function dayInfo(date: string): { weekday: string; date: string; relative: string } {
|
||||||
|
const title = capitalizeFirstLetter(formatRussianDate(date));
|
||||||
|
const space = title.indexOf(' ');
|
||||||
|
return {
|
||||||
|
weekday: space === -1 ? title : title.slice(0, space),
|
||||||
|
date: space === -1 ? '' : title.slice(space + 1),
|
||||||
|
relative: getRelativeDayName(date),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function badgeClass(relative: string): string {
|
||||||
|
switch (relative) {
|
||||||
|
case 'сегодня':
|
||||||
|
return 'badge--today';
|
||||||
|
case 'завтра':
|
||||||
|
return 'badge--tomorrow';
|
||||||
|
case 'послезавтра':
|
||||||
|
return 'badge--day-after';
|
||||||
|
case 'вчера':
|
||||||
|
return 'badge--yesterday';
|
||||||
|
default:
|
||||||
|
return 'badge--soon';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateLabel = computed(() => {
|
||||||
|
if (!schedule.value.updateTime) return 'время обновления неизвестно';
|
||||||
|
return timeAgo(schedule.value.updateTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
function showCollection(p: Performance): boolean {
|
||||||
|
return p.timeCollection !== '' && p.timeCollection !== '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPlace(p: Performance): boolean {
|
||||||
|
return p.place !== '' && p.place !== '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCostumes(p: Performance): boolean {
|
||||||
|
return p.costumes !== '' && p.costumes !== '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTime(p: Performance): boolean {
|
||||||
|
return p.timeStart !== '' && p.timeStart !== '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeSummary = { primary: string; caption: string; secondary: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Иерархия времени: «сбор» — главное, «начало» — дополнительное.
|
||||||
|
*/
|
||||||
|
function timeSummary(p: Performance): TimeSummary {
|
||||||
|
if (showCollection(p)) {
|
||||||
|
return {
|
||||||
|
primary: cleanTime(p.timeCollection),
|
||||||
|
caption: 'сбор',
|
||||||
|
secondary: showTime(p) ? `начало ${formatTimeRange(p.timeStart)}` : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (showTime(p)) {
|
||||||
|
return { primary: formatTimeRange(p.timeStart), caption: 'начало', secondary: '' };
|
||||||
|
}
|
||||||
|
return { primary: 'Время уточняется', caption: '', secondary: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = computed(() =>
|
||||||
|
schedule.value.days.map((day) => ({
|
||||||
|
...day,
|
||||||
|
info: dayInfo(day.date),
|
||||||
|
performances: day.performances.map((p) => ({ ...p, time: timeSummary(p) })),
|
||||||
|
})),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="schedule">
|
||||||
<div v-for="day in schedule.days" :key="day.date" class="day-block">
|
<header class="schedule__header">
|
||||||
{{ capitalizeFirstLetter(formatRussianDate(day.date)) }}
|
<div class="schedule__heading">
|
||||||
<span v-if="getRelativeDayName(day.date) !== ''" class="relative-day-name">{{ getRelativeDayName(day.date)
|
<h1 class="schedule__title">Родники</h1>
|
||||||
}}</span>
|
<p class="schedule__subtitle">Расписание выступлений</p>
|
||||||
<div v-for="(performance, index) in day.performances" :key="performance.name" class="performance-block">
|
|
||||||
<hr v-if="index > 0" class="hr">
|
|
||||||
<div v-if="performance.timeCollection !== '' && performance.timeCollection !== '-'"
|
|
||||||
class="time-collection-block">
|
|
||||||
Сбор: {{ cleanTime(performance.timeCollection) }}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="schedule__header-actions">
|
||||||
Время:
|
<button
|
||||||
<span v-if="performance.timeStart !== '' && performance.timeStart !== '-'">{{
|
type="button"
|
||||||
formatTimeRange(performance.timeStart) }}</span>
|
class="icon-button"
|
||||||
<span v-else>Уточняется</span>
|
:aria-label="themeLabel"
|
||||||
|
:title="themeLabel"
|
||||||
|
@click="toggleTheme"
|
||||||
|
>
|
||||||
|
<IconMoon v-if="theme === 'light'" class="icon-button__icon" />
|
||||||
|
<IconSun v-else class="icon-button__icon" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon-button"
|
||||||
|
:class="{ 'icon-button--loading': loading }"
|
||||||
|
:disabled="loading"
|
||||||
|
aria-label="Обновить расписание"
|
||||||
|
title="Обновить расписание"
|
||||||
|
@click="load"
|
||||||
|
>
|
||||||
|
<IconRefresh class="icon-button__icon" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="place-block">
|
</header>
|
||||||
Место: {{ capitalizeFirstLetter(performance.place) }}
|
|
||||||
|
<main class="schedule__content">
|
||||||
|
<!-- Загрузка -->
|
||||||
|
<template v-if="loading">
|
||||||
|
<p class="visually-hidden" role="status">Загрузка расписания</p>
|
||||||
|
<section v-for="n in 2" :key="n" class="day day--skeleton" aria-hidden="true">
|
||||||
|
<div class="skeleton skeleton--title"></div>
|
||||||
|
<div class="skeleton skeleton--line"></div>
|
||||||
|
<div class="skeleton skeleton--short"></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Ошибка -->
|
||||||
|
<div v-else-if="error" class="message-card" role="alert">
|
||||||
|
<p class="message-card__text">Не удалось загрузить расписание</p>
|
||||||
|
<p class="message-card__hint">Проверьте подключение к сети и попробуйте ещё раз</p>
|
||||||
|
<button type="button" class="button" @click="load">Попробовать снова</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
<!-- Пустое расписание -->
|
||||||
|
<div v-else-if="days.length === 0" class="message-card">
|
||||||
|
<p class="message-card__text">Расписание пока пусто</p>
|
||||||
|
<p class="message-card__hint">Загляните сюда позже — выступления скоро появятся</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Дни -->
|
||||||
|
<template v-else>
|
||||||
|
<section v-for="day in days" :key="day.date" class="day">
|
||||||
|
<header class="day__header">
|
||||||
|
<h2 class="day__title">
|
||||||
|
<span class="day__weekday">{{ day.info.weekday }}</span>
|
||||||
|
<span v-if="day.info.date" class="day__date">{{ day.info.date }}</span>
|
||||||
|
</h2>
|
||||||
|
<span
|
||||||
|
v-if="day.info.relative"
|
||||||
|
class="badge"
|
||||||
|
:class="badgeClass(day.info.relative)"
|
||||||
|
>
|
||||||
|
{{ day.info.relative }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p v-if="day.performances.length === 0" class="day__empty">
|
||||||
|
В этот день выступлений нет
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<article
|
||||||
|
v-for="performance in day.performances"
|
||||||
|
:key="performance.name"
|
||||||
|
class="performance"
|
||||||
|
>
|
||||||
|
<div class="performance__head">
|
||||||
|
<div class="performance__time">
|
||||||
|
<div class="performance__time-primary">
|
||||||
|
<span
|
||||||
|
class="performance__time-main"
|
||||||
|
:class="{ 'performance__time-main--muted': performance.time.caption === '' }"
|
||||||
|
>
|
||||||
|
{{ performance.time.primary }}
|
||||||
|
</span>
|
||||||
|
<span v-if="performance.time.caption" class="performance__time-caption">
|
||||||
|
{{ performance.time.caption }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="performance.time.secondary" class="performance__time-start">
|
||||||
|
<IconClock class="performance__icon" />
|
||||||
|
{{ performance.time.secondary }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="performance__info">
|
||||||
|
<h3 class="performance__name">
|
||||||
{{ capitalizeFirstLetter(performance.name) }}
|
{{ capitalizeFirstLetter(performance.name) }}
|
||||||
</div>
|
</h3>
|
||||||
<div v-if="performance.numbers.length !== 0">
|
<p v-if="showPlace(performance)" class="performance__place">
|
||||||
Номера:
|
<IconPin class="performance__icon" />
|
||||||
<div v-for="number in performance.numbers" :key="number.name" class="number-block">
|
{{ capitalizeFirstLetter(performance.place) }}
|
||||||
- {{ capitalizeFirstLetter(number.name) }}
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="performance.costumes !== '' && performance.costumes !== '-'">
|
|
||||||
|
<div v-if="performance.numbers.length > 0" class="performance__numbers">
|
||||||
|
<p class="performance__section-label">
|
||||||
|
<IconMusic class="performance__icon" />
|
||||||
|
Номера
|
||||||
|
</p>
|
||||||
|
<ul class="performance__list">
|
||||||
|
<li
|
||||||
|
v-for="number in performance.numbers"
|
||||||
|
:key="number.name"
|
||||||
|
class="performance__list-item"
|
||||||
|
>
|
||||||
|
{{ capitalizeFirstLetter(number.name) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showCostumes(performance)" class="performance__costumes">
|
||||||
|
<p class="performance__section-label">
|
||||||
|
<IconSparkles class="performance__icon" />
|
||||||
|
Костюмы
|
||||||
|
</p>
|
||||||
|
<p class="performance__costumes-text">
|
||||||
{{ capitalizeFirstLetter(performance.costumes) }}
|
{{ capitalizeFirstLetter(performance.costumes) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
</section>
|
||||||
<div class="footer">
|
</template>
|
||||||
Обновлено: {{ timeAgo(schedule.updateTime) }}
|
</main>
|
||||||
</div>
|
|
||||||
|
<footer v-if="!loading && !error && days.length > 0" class="schedule__footer">
|
||||||
|
<span class="schedule__footer-dot" aria-hidden="true"></span>
|
||||||
|
<span>Обновлено: {{ updateLabel }}</span>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.day-block {
|
.schedule {
|
||||||
margin: 10px 0;
|
display: flex;
|
||||||
padding: 15px;
|
flex-direction: column;
|
||||||
border-radius: 10px;
|
gap: 22px;
|
||||||
font-weight: 700;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-collection-block {
|
/* ---------- Шапка ---------- */
|
||||||
font-size: 20px;
|
.schedule__header {
|
||||||
font-weight: 700;
|
display: flex;
|
||||||
line-height: 25px;
|
align-items: flex-start;
|
||||||
margin: 3px 0;
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding-block: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-block {
|
.schedule__title {
|
||||||
font-weight: 700;
|
font-family: var(--font-display);
|
||||||
}
|
font-size: clamp(30px, 6vw, 42px);
|
||||||
|
|
||||||
.performance-block {}
|
|
||||||
|
|
||||||
.number-block {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hr {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.relative-day-name {
|
|
||||||
background-color: #70e5ff;
|
|
||||||
padding: 3px 10px 5px 10px;
|
|
||||||
border-radius: 15px;
|
|
||||||
float: right;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: var(--c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__subtitle {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--c-ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid var(--c-line-strong);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--c-surface);
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover:not(:disabled) {
|
||||||
|
color: var(--c-accent);
|
||||||
|
border-color: var(--c-accent);
|
||||||
|
background: var(--c-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:focus-visible {
|
||||||
|
outline: 2px solid var(--c-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button--loading .icon-button__icon {
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Карточка дня ---------- */
|
||||||
|
.schedule__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
background: var(--c-surface);
|
||||||
|
border: 1px solid var(--c-line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: clamp(18px, 3.5vw, 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 12px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 1px solid var(--c-line);
|
||||||
|
}
|
||||||
|
.day__title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(20px, 4vw, 26px);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day__weekday {
|
||||||
|
color: var(--c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day__date {
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day__empty {
|
||||||
|
padding: 18px 4px 6px;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--c-ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
flex: none;
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge::before {
|
||||||
|
content: '';
|
||||||
|
flex: none;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--today {
|
||||||
|
background: linear-gradient(135deg, var(--c-accent), var(--c-violet));
|
||||||
|
color: var(--c-today-text);
|
||||||
|
box-shadow: 0 4px 14px -6px var(--c-today-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--tomorrow {
|
||||||
|
background: var(--c-violet-soft);
|
||||||
|
color: var(--c-violet-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--day-after {
|
||||||
|
background: var(--c-pink-soft);
|
||||||
|
color: var(--c-pink-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--yesterday {
|
||||||
|
background: var(--c-amber-soft);
|
||||||
|
color: var(--c-amber-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--soon {
|
||||||
|
background: var(--c-accent-soft);
|
||||||
|
color: var(--c-accent-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Выступление ---------- */
|
||||||
|
.performance {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance + .performance {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px dashed var(--c-line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 520px) {
|
||||||
|
.performance__head {
|
||||||
|
grid-template-columns: minmax(128px, 0.85fr) 2fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__icon {
|
||||||
|
flex: none;
|
||||||
|
width: 1.05em;
|
||||||
|
height: 1.05em;
|
||||||
|
color: var(--c-accent);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Время — главный якорь выступления (сбор важнее начала) */
|
||||||
|
.performance__time {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__time-primary {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__time-main {
|
||||||
|
font-size: clamp(22px, 4.5vw, 27px);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--c-accent);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
|
||||||
|
.performance__time-main {
|
||||||
|
background: linear-gradient(120deg, var(--c-accent), var(--c-violet));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__time-main--muted {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--c-ink-faint);
|
||||||
|
background: none;
|
||||||
|
-webkit-background-clip: initial;
|
||||||
|
background-clip: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__time-caption {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--c-violet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__time-start {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Название и место */
|
||||||
|
.performance__info {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__name {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__place {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__section-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__numbers {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__numbers .performance__icon {
|
||||||
|
color: var(--c-violet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Разноцветная ротация пилюль номеров */
|
||||||
|
.performance__list-item:nth-child(4n + 1) {
|
||||||
|
background: var(--c-accent-soft);
|
||||||
|
color: var(--c-accent-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list-item:nth-child(4n + 2) {
|
||||||
|
background: var(--c-violet-soft);
|
||||||
|
color: var(--c-violet-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list-item:nth-child(4n + 3) {
|
||||||
|
background: var(--c-pink-soft);
|
||||||
|
color: var(--c-pink-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list-item:nth-child(4n + 4) {
|
||||||
|
background: var(--c-amber-soft);
|
||||||
|
color: var(--c-amber-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__list-item::before {
|
||||||
|
content: '';
|
||||||
|
flex: none;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__costumes {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-left: 3px solid var(--c-amber);
|
||||||
|
border-radius: 0 var(--r-md) var(--r-md) 0;
|
||||||
|
background: var(--c-amber-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__costumes .performance__icon {
|
||||||
|
color: var(--c-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance__costumes-text {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Скелетон загрузки ---------- */
|
||||||
|
.day--skeleton {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--c-line) 25%,
|
||||||
|
var(--c-line-strong) 40%,
|
||||||
|
var(--c-line) 55%
|
||||||
|
);
|
||||||
|
background-size: 400% 100%;
|
||||||
|
animation: shimmer 1.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton--title {
|
||||||
|
width: 40%;
|
||||||
|
height: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton--line {
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton--short {
|
||||||
|
width: 58%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -100% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Сообщения (ошибка / пусто) ---------- */
|
||||||
|
.message-card {
|
||||||
|
padding: 44px 24px;
|
||||||
|
border: 1px solid var(--c-line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--c-surface);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card__text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--c-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card__hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--c-ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card .button {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: var(--c-accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: var(--c-accent-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus-visible {
|
||||||
|
outline: 2px solid var(--c-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Подвал ---------- */
|
||||||
|
.schedule__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--c-ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule__footer-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--c-accent);
|
||||||
|
animation: pulse 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export const apiGetDays = async (): Promise<Schedule> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getApiUrl(path: string) {
|
export function getApiUrl(path: string) {
|
||||||
const url = 'https://' + window.location.host.split(':')[0] + path
|
// const url = 'https://' + window.location.host.split(':')[0] + path
|
||||||
// const url = 'http://localhost:8210' + path
|
const url = 'http://localhost:8210' + path
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12 6 12 12 16 14" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M9 18V5l12-2v13" />
|
||||||
|
<circle cx="6" cy="18" r="3" />
|
||||||
|
<circle cx="18" cy="16" r="3" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M23 4v6h-6" />
|
||||||
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M12 3l2.2 6.6L21 12l-6.8 2.4L12 21l-2.2-6.6L3 12l6.8-2.4L12 3Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path d="M12 2v2" />
|
||||||
|
<path d="M12 20v2" />
|
||||||
|
<path d="m4.93 4.93 1.41 1.41" />
|
||||||
|
<path d="m17.66 17.66 1.41 1.41" />
|
||||||
|
<path d="M2 12h2" />
|
||||||
|
<path d="M20 12h2" />
|
||||||
|
<path d="m6.34 17.66-1.41 1.41" />
|
||||||
|
<path d="m19.07 4.93-1.41 1.41" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export type Theme = 'light' | 'dark'
|
||||||
|
|
||||||
|
const THEME_KEY = 'rodniki-theme'
|
||||||
|
const DARK_CANVAS = '#101a2c'
|
||||||
|
const LIGHT_CANVAS = '#ffffff'
|
||||||
|
|
||||||
|
const theme = ref<Theme>('light')
|
||||||
|
|
||||||
|
function readStoredTheme(): Theme | null {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(THEME_KEY)
|
||||||
|
if (stored === 'light' || stored === 'dark') return stored
|
||||||
|
} catch {
|
||||||
|
// localStorage может быть недоступен (приватный режим и т.п.)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemTheme(): Theme {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(value: Theme): void {
|
||||||
|
document.documentElement.setAttribute('data-theme', value)
|
||||||
|
document
|
||||||
|
.querySelector<HTMLMetaElement>('meta[name="theme-color"]')
|
||||||
|
?.setAttribute('content', value === 'dark' ? DARK_CANVAS : LIGHT_CANVAS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const initial = readStoredTheme() ?? systemTheme()
|
||||||
|
theme.value = initial
|
||||||
|
applyTheme(initial)
|
||||||
|
|
||||||
|
// Пока пользователь не выбрал тему вручную — следуем за системной
|
||||||
|
if (readStoredTheme() === null) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
||||||
|
theme.value = event.matches ? 'dark' : 'light'
|
||||||
|
applyTheme(theme.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
function toggleTheme(): void {
|
||||||
|
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
||||||
|
try {
|
||||||
|
localStorage.setItem(THEME_KEY, theme.value)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
applyTheme(theme.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { theme, toggleTheme }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user