feat(nav): add in-dev sections and coming soon placeholders
Add navigation links for Dish, Events, Actions, Dream Island, and Clothes. Implement StatusBadge component and ComingSoonView for future content.
This commit is contained in:
@@ -3,7 +3,20 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import AppShell from './components/AppShell.vue';
|
||||
import { iconAdmin, iconChecklist, iconHabitat, iconItem, iconLife, iconPokemon, iconRecipe } from './icons';
|
||||
import {
|
||||
iconAction,
|
||||
iconAdmin,
|
||||
iconChecklist,
|
||||
iconClothes,
|
||||
iconDish,
|
||||
iconDreamIsland,
|
||||
iconEvent,
|
||||
iconHabitat,
|
||||
iconItem,
|
||||
iconLife,
|
||||
iconPokemon,
|
||||
iconRecipe
|
||||
} from './icons';
|
||||
import { getCurrentLocale, onLocaleChange, setCurrentLocale } from './i18n';
|
||||
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
|
||||
|
||||
@@ -18,11 +31,20 @@ const languages = ref<Language[]>([
|
||||
let removeAuthListener: (() => void) | null = null;
|
||||
let removeLocaleListener: (() => void) | null = null;
|
||||
|
||||
function inDevBadge() {
|
||||
return { label: t('common.inDev'), tone: 'info' as const };
|
||||
}
|
||||
|
||||
const navItems = computed(() => [
|
||||
{ label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon },
|
||||
{ label: t('nav.habitats'), to: '/habitats', icon: iconHabitat },
|
||||
{ label: t('nav.items'), to: '/items', icon: iconItem },
|
||||
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
|
||||
{ label: t('nav.dish'), to: '/dish', icon: iconDish, badge: inDevBadge() },
|
||||
{ label: t('nav.events'), to: '/events', icon: iconEvent, badge: inDevBadge() },
|
||||
{ label: t('nav.actions'), to: '/actions', icon: iconAction, badge: inDevBadge() },
|
||||
{ label: t('nav.dreamIsland'), to: '/dream-island', icon: iconDreamIsland, badge: inDevBadge() },
|
||||
{ label: t('nav.clothes'), to: '/clothes', icon: iconClothes, badge: inDevBadge() },
|
||||
{ label: t('nav.checklist'), to: '/checklist', icon: iconChecklist },
|
||||
{ label: t('nav.life'), to: '/life', icon: iconLife },
|
||||
{ label: t('nav.admin'), to: '/admin', icon: iconAdmin }
|
||||
|
||||
@@ -6,12 +6,21 @@ import { useRoute } from 'vue-router';
|
||||
import { iconClose, iconLogin, iconLogout, iconMenu, iconRegister, iconTranslate, type AppIcon } from '../icons';
|
||||
import type { AuthUser, Language } from '../services/api';
|
||||
import PokeBallMark from './PokeBallMark.vue';
|
||||
import StatusBadge from './StatusBadge.vue';
|
||||
|
||||
defineProps<{
|
||||
currentUser: AuthUser | null;
|
||||
languages: Language[];
|
||||
locale: string;
|
||||
navItems: Array<{ label: string; to: string; icon?: AppIcon }>;
|
||||
navItems: Array<{
|
||||
label: string;
|
||||
to: string;
|
||||
icon?: AppIcon;
|
||||
badge?: {
|
||||
label: string;
|
||||
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
||||
};
|
||||
}>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -132,7 +141,14 @@ onBeforeUnmount(() => {
|
||||
@click="closeSidebar"
|
||||
>
|
||||
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="side-nav__label">{{ item.label }}</span>
|
||||
<StatusBadge
|
||||
v-if="item.badge"
|
||||
class="side-nav__badge"
|
||||
:label="item.badge.label"
|
||||
:tone="item.badge.tone"
|
||||
compact
|
||||
/>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
|
||||
20
frontend/src/components/StatusBadge.vue
Normal file
20
frontend/src/components/StatusBadge.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
label: string;
|
||||
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
|
||||
compact?: boolean;
|
||||
}>(),
|
||||
{
|
||||
tone: 'info',
|
||||
compact: false
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="status-badge" :class="[`status-badge--${tone}`, { 'status-badge--compact': compact }]">
|
||||
<span class="status-badge__dot" aria-hidden="true"></span>
|
||||
<span class="status-badge__label">{{ label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -35,6 +35,7 @@ const messages = {
|
||||
noMatches: 'No matches',
|
||||
createNamed: 'Add "{name}"',
|
||||
creating: 'Adding',
|
||||
inDev: 'In-Dev',
|
||||
removeNamed: 'Remove {name}',
|
||||
quantity: 'Quantity',
|
||||
required: 'Required'
|
||||
@@ -44,6 +45,11 @@ const messages = {
|
||||
habitats: 'Habitats',
|
||||
items: 'Items',
|
||||
recipes: 'Recipes',
|
||||
dish: 'Dish',
|
||||
events: 'Events',
|
||||
actions: 'Actions',
|
||||
dreamIsland: 'Dream Island',
|
||||
clothes: 'Clothes',
|
||||
checklist: 'CheckList',
|
||||
life: 'Life',
|
||||
admin: 'Admin',
|
||||
@@ -214,6 +220,68 @@ const messages = {
|
||||
materials: 'Materials',
|
||||
addMaterial: 'Add material'
|
||||
},
|
||||
comingSoon: {
|
||||
status: 'In development',
|
||||
heading: 'This wiki section is being prepared.',
|
||||
previewLabel: 'Section preview',
|
||||
sections: {
|
||||
dish: {
|
||||
kicker: 'Dish',
|
||||
title: 'Dish',
|
||||
subtitle: 'A future home for cooked dishes and food discoveries.',
|
||||
body: 'Dish pages are being shaped for clear browsing, source notes, and useful ingredient links.',
|
||||
preview: {
|
||||
one: 'Dish records will focus on names, effects, and discovery context.',
|
||||
two: 'Ingredient relationships will connect back to items and recipes where useful.',
|
||||
three: 'The page will stay browse-first so community edits can grow naturally.'
|
||||
}
|
||||
},
|
||||
events: {
|
||||
kicker: 'Events',
|
||||
title: 'Events',
|
||||
subtitle: 'Seasonal and limited-time game activity records are coming later.',
|
||||
body: 'Events will collect timing, rewards, and participation details once the section is ready.',
|
||||
preview: {
|
||||
one: 'Event cards will make dates and active windows easy to scan.',
|
||||
two: 'Rewards and related items will sit close to the event summary.',
|
||||
three: 'Archived activities will remain readable after they end.'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
kicker: 'Actions',
|
||||
title: 'Actions',
|
||||
subtitle: 'Game shortcut actions such as waving and dancing will be documented here.',
|
||||
body: 'Actions are being prepared as a quick reference for expressive in-game gestures and shortcuts.',
|
||||
preview: {
|
||||
one: 'Each action will describe the gesture or shortcut in player-facing language.',
|
||||
two: 'Common examples include waving, dancing, and other social actions.',
|
||||
three: 'Related unlock or usage details can be linked when the data model is ready.'
|
||||
}
|
||||
},
|
||||
dreamIsland: {
|
||||
kicker: 'Dream Island',
|
||||
title: 'Dream Island',
|
||||
subtitle: 'Dream Island information is being organized for future browsing.',
|
||||
body: 'This area will present island details with a calm, destination-style layout when content is ready.',
|
||||
preview: {
|
||||
one: 'Island notes will prioritize location, availability, and notable discoveries.',
|
||||
two: 'Related Pokemon, items, or activities can be connected from the page.',
|
||||
three: 'The layout will support browsing without adding another management flow yet.'
|
||||
}
|
||||
},
|
||||
clothes: {
|
||||
kicker: 'Clothes',
|
||||
title: 'Clothes',
|
||||
subtitle: 'Outfit and clothing references are being prepared.',
|
||||
body: 'Clothes pages will make it easy to compare appearance, acquisition, and customization details.',
|
||||
preview: {
|
||||
one: 'Clothing entries will focus on display names and visual categories.',
|
||||
two: 'Acquisition and customization details can be connected when available.',
|
||||
three: 'The page will keep item-like details readable without mixing them into the item list.'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
checklist: {
|
||||
title: 'Daily checklist',
|
||||
subtitle: 'See what can be completed each day.',
|
||||
@@ -396,6 +464,7 @@ const messages = {
|
||||
noMatches: '没有匹配项',
|
||||
createNamed: '添加「{name}」',
|
||||
creating: '添加中',
|
||||
inDev: '开发中',
|
||||
removeNamed: '移除{name}',
|
||||
quantity: '数量',
|
||||
required: '必填'
|
||||
@@ -405,6 +474,11 @@ const messages = {
|
||||
habitats: '栖息地',
|
||||
items: '物品',
|
||||
recipes: '材料单',
|
||||
dish: '料理',
|
||||
events: '活动',
|
||||
actions: '动作',
|
||||
dreamIsland: 'Dream Island',
|
||||
clothes: '服装',
|
||||
checklist: 'CheckList',
|
||||
life: 'Life',
|
||||
admin: '管理',
|
||||
@@ -575,6 +649,68 @@ const messages = {
|
||||
materials: '需要材料',
|
||||
addMaterial: '添加材料'
|
||||
},
|
||||
comingSoon: {
|
||||
status: '正在开发中',
|
||||
heading: '这个 Wiki 分区正在准备中。',
|
||||
previewLabel: '分区预览',
|
||||
sections: {
|
||||
dish: {
|
||||
kicker: 'Dish',
|
||||
title: '料理',
|
||||
subtitle: '未来会用于整理料理和食物相关发现。',
|
||||
body: '料理页面会围绕清晰浏览、来源记录和材料关联来设计。',
|
||||
preview: {
|
||||
one: '料理记录会优先呈现名称、效果和发现方式。',
|
||||
two: '需要时会把材料关系连接回物品和材料单。',
|
||||
three: '页面会先保持浏览友好,后续再自然承接社区编辑内容。'
|
||||
}
|
||||
},
|
||||
events: {
|
||||
kicker: 'Events',
|
||||
title: '活动',
|
||||
subtitle: '季节活动和限时内容资料会在这里整理。',
|
||||
body: '活动分区会在准备好后集中展示时间、奖励和参与信息。',
|
||||
preview: {
|
||||
one: '活动卡片会让日期和开放时间更容易浏览。',
|
||||
two: '奖励与关联物品会靠近活动摘要展示。',
|
||||
three: '活动结束后,历史记录也会保持可读。'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
kicker: 'Actions',
|
||||
title: '动作',
|
||||
subtitle: '挥手、跳舞等游戏内快捷动作会记录在这里。',
|
||||
body: '动作分区会作为游戏内表情、社交动作和快捷动作的快速参考。',
|
||||
preview: {
|
||||
one: '每个动作会用面向玩家的语言说明动作或快捷方式。',
|
||||
two: '常见内容包括挥手、跳舞和其他社交动作。',
|
||||
three: '后续可在数据模型准备好后补充解锁或使用条件。'
|
||||
}
|
||||
},
|
||||
dreamIsland: {
|
||||
kicker: 'Dream Island',
|
||||
title: 'Dream Island',
|
||||
subtitle: 'Dream Island 相关资料正在整理。',
|
||||
body: '这个区域未来会用更像目的地资料页的方式展示岛屿信息。',
|
||||
preview: {
|
||||
one: '岛屿记录会优先整理地点、开放状态和重要发现。',
|
||||
two: '可关联的 Pokemon、物品或活动会从页面中连接出来。',
|
||||
three: '目前先保持公开浏览入口,不额外增加管理流程。'
|
||||
}
|
||||
},
|
||||
clothes: {
|
||||
kicker: 'Clothes',
|
||||
title: '服装',
|
||||
subtitle: '外观和服装资料正在准备。',
|
||||
body: '服装页面会用于对比外观、入手方式和自定义信息。',
|
||||
preview: {
|
||||
one: '服装条目会优先整理展示名称和视觉分类。',
|
||||
two: '入手方式与自定义信息会在资料可用后接入。',
|
||||
three: '页面会保持服装资料清晰,不和普通物品列表混在一起。'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
checklist: {
|
||||
title: '每日清单',
|
||||
subtitle: '查看每天可以完成的事项。',
|
||||
|
||||
@@ -2,6 +2,7 @@ export type AppIcon = string;
|
||||
|
||||
export const iconAdd: AppIcon = 'mdi:plus';
|
||||
export const iconAdmin: AppIcon = 'mdi:tune-variant';
|
||||
export const iconAction: AppIcon = 'mdi:gesture-tap-button';
|
||||
export const iconBack: AppIcon = 'mdi:arrow-left';
|
||||
export const iconCancel: AppIcon = 'mdi:close';
|
||||
export const iconCheck: AppIcon = 'mdi:check';
|
||||
@@ -10,13 +11,17 @@ export const iconChevronDown: AppIcon = 'mdi:chevron-down';
|
||||
export const iconClose: AppIcon = 'mdi:close';
|
||||
export const iconComment: AppIcon = 'mdi:comment-outline';
|
||||
export const iconDelete: AppIcon = 'mdi:trash-can-outline';
|
||||
export const iconDish: AppIcon = 'mdi:silverware-fork-knife';
|
||||
export const iconDragHandle: AppIcon = 'mdi:drag';
|
||||
export const iconDreamIsland: AppIcon = 'mdi:palm-tree';
|
||||
export const iconEdit: AppIcon = 'mdi:pencil-outline';
|
||||
export const iconError: AppIcon = 'mdi:close-circle-outline';
|
||||
export const iconEvent: AppIcon = 'mdi:calendar-star';
|
||||
export const iconHabitat: AppIcon = 'mdi:pine-tree';
|
||||
export const iconInfo: AppIcon = 'mdi:information-outline';
|
||||
export const iconItem: AppIcon = 'mdi:bag-personal-outline';
|
||||
export const iconLife: AppIcon = 'mdi:post-outline';
|
||||
export const iconClothes: AppIcon = 'mdi:tshirt-crew-outline';
|
||||
export const iconLogin: AppIcon = 'mdi:login';
|
||||
export const iconLogout: AppIcon = 'mdi:logout';
|
||||
export const iconMail: AppIcon = 'mdi:email-fast-outline';
|
||||
|
||||
@@ -9,6 +9,7 @@ import RecipeList from '../views/RecipeList.vue';
|
||||
import RecipeDetail from '../views/RecipeDetail.vue';
|
||||
import DailyChecklistView from '../views/DailyChecklistView.vue';
|
||||
import LifeView from '../views/LifeView.vue';
|
||||
import ComingSoonView from '../views/ComingSoonView.vue';
|
||||
import AdminView from '../views/AdminView.vue';
|
||||
import LoginView from '../views/LoginView.vue';
|
||||
import RegisterView from '../views/RegisterView.vue';
|
||||
@@ -35,6 +36,11 @@ export const router = createRouter({
|
||||
{ path: '/recipes/new', name: 'recipe-new', component: RecipeList, meta: { requiresVerified: true, editorModal: true } },
|
||||
{ path: '/recipes/:id/edit', name: 'recipe-edit', component: RecipeDetail, meta: { requiresVerified: true, editorModal: true } },
|
||||
{ path: '/recipes/:id', name: 'recipe-detail', component: RecipeDetail },
|
||||
{ path: '/dish', name: 'dish', component: ComingSoonView, props: { page: 'dish' } },
|
||||
{ path: '/events', name: 'events', component: ComingSoonView, props: { page: 'events' } },
|
||||
{ path: '/actions', name: 'actions', component: ComingSoonView, props: { page: 'actions' } },
|
||||
{ path: '/dream-island', name: 'dream-island', component: ComingSoonView, props: { page: 'dreamIsland' } },
|
||||
{ path: '/clothes', name: 'clothes', component: ComingSoonView, props: { page: 'clothes' } },
|
||||
{ path: '/checklist', component: DailyChecklistView },
|
||||
{ path: '/life', component: LifeView },
|
||||
{ path: '/admin', component: AdminView, meta: { requiresVerified: true } },
|
||||
|
||||
@@ -208,11 +208,32 @@ svg {
|
||||
box-shadow: 0 2px 0 var(--line-strong);
|
||||
}
|
||||
|
||||
.side-nav__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.side-nav__icon {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.side-nav__badge {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.side-nav__link.router-link-active .status-badge {
|
||||
border-color: rgba(255, 255, 255, 0.34);
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.side-nav__link.router-link-active .status-badge__dot {
|
||||
background: var(--pokemon-yellow);
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: grid;
|
||||
align-content: end;
|
||||
@@ -1902,6 +1923,222 @@ button:disabled,
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
--status-color: var(--muted);
|
||||
width: fit-content;
|
||||
min-height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--status-color) 34%, var(--line));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--status-color) 10%, var(--surface-soft));
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
line-height: 1.1;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge--compact {
|
||||
min-height: 22px;
|
||||
gap: 4px;
|
||||
padding: 3px 6px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.status-badge__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--status-color);
|
||||
}
|
||||
|
||||
.status-badge__label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-badge--info {
|
||||
--status-color: var(--pokemon-blue);
|
||||
}
|
||||
|
||||
.status-badge--success {
|
||||
--status-color: var(--success);
|
||||
}
|
||||
|
||||
.status-badge--warning {
|
||||
--status-color: var(--warning);
|
||||
}
|
||||
|
||||
.status-badge--danger {
|
||||
--status-color: var(--danger);
|
||||
}
|
||||
|
||||
.status-badge--neutral {
|
||||
--status-color: var(--muted);
|
||||
}
|
||||
|
||||
.coming-soon-panel {
|
||||
--soon-accent: var(--pokemon-blue);
|
||||
--soon-accent-soft: color-mix(in srgb, var(--soon-accent) 14%, var(--surface));
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) minmax(160px, 0.32fr);
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
padding: 24px;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: var(--radius-card);
|
||||
background:
|
||||
linear-gradient(135deg, var(--soon-accent-soft), transparent 62%),
|
||||
linear-gradient(180deg, var(--surface) 0%, var(--surface-soft) 100%);
|
||||
box-shadow: var(--shadow-control);
|
||||
}
|
||||
|
||||
.coming-soon-panel--dish {
|
||||
--soon-accent: var(--pokemon-yellow);
|
||||
}
|
||||
|
||||
.coming-soon-panel--events {
|
||||
--soon-accent: var(--pokemon-red);
|
||||
}
|
||||
|
||||
.coming-soon-panel--actions {
|
||||
--soon-accent: var(--pokemon-blue);
|
||||
}
|
||||
|
||||
.coming-soon-panel--dream {
|
||||
--soon-accent: var(--success);
|
||||
}
|
||||
|
||||
.coming-soon-panel--clothes {
|
||||
--soon-accent: var(--type-psychic);
|
||||
}
|
||||
|
||||
.coming-soon-panel__icon {
|
||||
width: clamp(76px, 11vw, 118px);
|
||||
aspect-ratio: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--soon-accent);
|
||||
box-shadow: 0 5px 0 var(--line-strong);
|
||||
color: #172036;
|
||||
}
|
||||
|
||||
.coming-soon-panel--events .coming-soon-panel__icon,
|
||||
.coming-soon-panel--actions .coming-soon-panel__icon,
|
||||
.coming-soon-panel--dream .coming-soon-panel__icon,
|
||||
.coming-soon-panel--clothes .coming-soon-panel__icon {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.coming-soon-panel__icon .ui-icon {
|
||||
width: 54%;
|
||||
height: 54%;
|
||||
}
|
||||
|
||||
.coming-soon-panel__copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coming-soon-panel__copy h2 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(28px, 4vw, 46px);
|
||||
font-weight: 950;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.coming-soon-panel__copy p {
|
||||
max-width: 62ch;
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal {
|
||||
height: 100%;
|
||||
min-height: 180px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal span {
|
||||
display: block;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: var(--radius-small);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--soon-accent) 72%, #ffffff), var(--soon-accent)),
|
||||
var(--soon-accent);
|
||||
box-shadow: 0 3px 0 var(--line-strong);
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal span:nth-child(1) {
|
||||
height: 46%;
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal span:nth-child(2) {
|
||||
height: 72%;
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal span:nth-child(3) {
|
||||
height: 58%;
|
||||
}
|
||||
|
||||
.coming-soon-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.coming-soon-preview__item {
|
||||
min-height: 128px;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.coming-soon-preview__index {
|
||||
width: 42px;
|
||||
min-height: 30px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--surface-soft);
|
||||
color: var(--pokemon-blue-deep);
|
||||
font-size: 13px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.coming-soon-preview__item p {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.reorderable-row {
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
@@ -3077,6 +3314,19 @@ button:disabled,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.coming-soon-panel {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 94px;
|
||||
}
|
||||
|
||||
.coming-soon-preview {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.appearance-row__main {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -3106,7 +3356,8 @@ button:disabled,
|
||||
.filter-panel,
|
||||
.toolbar,
|
||||
.entity-grid,
|
||||
.grid {
|
||||
.grid,
|
||||
.coming-soon-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -3114,6 +3365,15 @@ button:disabled,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.coming-soon-panel {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.coming-soon-panel__signal {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.life-post__header {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
82
frontend/src/views/ComingSoonView.vue
Normal file
82
frontend/src/views/ComingSoonView.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import StatusBadge from '../components/StatusBadge.vue';
|
||||
import {
|
||||
iconAction,
|
||||
iconClothes,
|
||||
iconDish,
|
||||
iconDreamIsland,
|
||||
iconEvent,
|
||||
type AppIcon
|
||||
} from '../icons';
|
||||
|
||||
type ComingSoonPage = 'dish' | 'events' | 'actions' | 'dreamIsland' | 'clothes';
|
||||
|
||||
type ComingSoonConfig = {
|
||||
icon: AppIcon;
|
||||
accent: 'dish' | 'events' | 'actions' | 'dream' | 'clothes';
|
||||
previewKeys: Array<'one' | 'two' | 'three'>;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
page: ComingSoonPage;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const pageConfigByPage: Record<ComingSoonPage, ComingSoonConfig> = {
|
||||
dish: { icon: iconDish, accent: 'dish', previewKeys: ['one', 'two', 'three'] },
|
||||
events: { icon: iconEvent, accent: 'events', previewKeys: ['one', 'two', 'three'] },
|
||||
actions: { icon: iconAction, accent: 'actions', previewKeys: ['one', 'two', 'three'] },
|
||||
dreamIsland: { icon: iconDreamIsland, accent: 'dream', previewKeys: ['one', 'two', 'three'] },
|
||||
clothes: { icon: iconClothes, accent: 'clothes', previewKeys: ['one', 'two', 'three'] }
|
||||
};
|
||||
|
||||
const pageConfig = computed(() => pageConfigByPage[props.page]);
|
||||
const previewItems = computed(() =>
|
||||
pageConfig.value.previewKeys.map((previewKey, index) => ({
|
||||
code: String(index + 1).padStart(2, '0'),
|
||||
text: t(pageMessageKey(`preview.${previewKey}`))
|
||||
}))
|
||||
);
|
||||
|
||||
function pageMessageKey(suffix: string) {
|
||||
return `pages.comingSoon.sections.${props.page}.${suffix}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-stack coming-soon-page">
|
||||
<PageHeader :title="t(pageMessageKey('title'))" :subtitle="t(pageMessageKey('subtitle'))">
|
||||
<template #kicker>{{ t(pageMessageKey('kicker')) }}</template>
|
||||
</PageHeader>
|
||||
|
||||
<section class="coming-soon-panel" :class="`coming-soon-panel--${pageConfig.accent}`">
|
||||
<div class="coming-soon-panel__icon" aria-hidden="true">
|
||||
<Icon :icon="pageConfig.icon" class="ui-icon" />
|
||||
</div>
|
||||
|
||||
<div class="coming-soon-panel__copy">
|
||||
<StatusBadge :label="t('pages.comingSoon.status')" tone="info" />
|
||||
<h2>{{ t('pages.comingSoon.heading') }}</h2>
|
||||
<p>{{ t(pageMessageKey('body')) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="coming-soon-panel__signal" aria-hidden="true">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="coming-soon-preview" :aria-label="t('pages.comingSoon.previewLabel')">
|
||||
<article v-for="item in previewItems" :key="item.code" class="coming-soon-preview__item">
|
||||
<span class="coming-soon-preview__index">{{ item.code }}</span>
|
||||
<p>{{ item.text }}</p>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user