484 lines
13 KiB
Vue
484 lines
13 KiB
Vue
<template>
|
||
<div> <!-- Removed .page-container for a more open layout -->
|
||
<!-- ================================= -->
|
||
<!-- Authenticated User Dashboard -->
|
||
<!-- ================================= -->
|
||
<div v-if="isAuthenticated && user" class="dashboard-content page-container">
|
||
|
||
<h1>Привет, {{ user.nickname }}!</h1>
|
||
|
||
<div class="habits-section">
|
||
<div v-if="habitsPending">Загрузка привычек...</div>
|
||
<div v-else-if="habitsError">Не удалось загрузить привычки.</div>
|
||
<div v-else-if="habits && habits.length > 0">
|
||
<HabitCard
|
||
v-for="habit in habits"
|
||
:key="habit.id"
|
||
:habit="habit"
|
||
:is-submitting-habit="isSubmittingHabit"
|
||
:exploding-habit-id="explodingHabitId"
|
||
@complete="completeHabit"
|
||
/>
|
||
</div>
|
||
<div v-else>
|
||
<p>У вас еще нет привычек. Перейдите на страницу <NuxtLink to="/habits">Мои привычки</NuxtLink>, чтобы создать их.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="text-color-light">Ваши цели обновляются раз в сутки. Получаемые бонусы усиливаются, если посещать сайт ежедневно.</p>
|
||
|
||
<div class="streak-section">
|
||
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 1 }">
|
||
<h2>x1</h2>
|
||
<p>Базовые</p>
|
||
</div>
|
||
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 2 }">
|
||
<h2>x2</h2>
|
||
<p>Двойные</p>
|
||
</div>
|
||
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak >= 3 }">
|
||
<h2>x3</h2>
|
||
<p>Тройные</p>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- ================================= -->
|
||
<!-- Anonymous User Onboarding Funnel -->
|
||
<!-- ================================= -->
|
||
<div v-else-if="isAnonymous" class="onboarding-container">
|
||
<OnboardingFunnel />
|
||
</div>
|
||
|
||
<!-- ================================= -->
|
||
<!-- New/Unidentified User Welcome -->
|
||
<!-- ================================= -->
|
||
<div v-else class="welcome-content">
|
||
<h1>Трекер привычек с геймификацией</h1>
|
||
<p class="subtitle">Создавайте и отслеживайте свои привычки и развивайте свою деревню.</p>
|
||
<div class="auth-buttons">
|
||
<button @click="startOnboarding" class="btn btn-primary">Начать!</button>
|
||
<NuxtLink to="/login" class="btn btn-secondary">Уже есть аккаунт</NuxtLink>
|
||
</div>
|
||
|
||
<!-- HR replaced with spacing via CSS -->
|
||
<div class="marketing-divider"></div>
|
||
|
||
<div class="marketing-container">
|
||
<h3>Попробуйте сами!</h3>
|
||
|
||
<div class="demo-habit-section">
|
||
<div class="arrow-with-text to-button">
|
||
<span>Отслеживайте выполнение привычек...</span>
|
||
</div>
|
||
<div class="demo-habit-card">
|
||
<span :class="{ 'completed': isHabitCompleting }">{{ currentHabitText }}</span>
|
||
<button @click="advanceMarketingDemo" class="btn btn-sm btn-success" :disabled="isHabitCompleting">Готово</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="demo-village-section">
|
||
<div class="arrow-with-text to-village" :class="{ 'visible': showVillageArrow }">
|
||
<span>...и стройте деревню! ↓</span>
|
||
</div>
|
||
<div class="demo-village-grid">
|
||
<div
|
||
v-for="(content, index) in villageSquares"
|
||
:key="index"
|
||
class="village-square-demo"
|
||
:class="{
|
||
'initial-obstacle': villageInitialState[index] && content === villageInitialState[index],
|
||
'built': content && content !== villageInitialState[index],
|
||
'empty': !content
|
||
}"
|
||
>
|
||
<span class="emoji-content">{{ content }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
|
||
// Use the refactored auth composable
|
||
const { user, isAuthenticated, isAnonymous, updateUser, startOnboarding } = useAuth();
|
||
const api = useApi();
|
||
|
||
// --- Habits Data (This part only runs for authenticated users but is fine to leave here) ---
|
||
const { data: habits, pending: habitsPending, error: habitsError, refresh: refreshHabits } = await useFetch('/api/habits', {
|
||
lazy: true,
|
||
server: false,
|
||
});
|
||
|
||
|
||
// --- Marketing Demo State (for anonymous users) ---
|
||
const demoHabits = ref([
|
||
'Читать 20 минут в день',
|
||
'Делать 50 приседаний',
|
||
'Выпить стакан воды',
|
||
'Позвонить маме',
|
||
'Учить новый язык (15 минут)',
|
||
'Медитировать 5 минут',
|
||
'Прогуляться на свежем воздухе',
|
||
'Запланировать завтрашний день',
|
||
'Начать сначала!',
|
||
]);
|
||
const currentHabitText = ref(demoHabits.value[0]);
|
||
const isHabitCompleting = ref(false);
|
||
|
||
const marketingStep = ref(0);
|
||
const showVillageArrow = ref(false);
|
||
const villageInitialState = ['', '', '', '', '🌳', '🪨'];
|
||
const villageSquares = ref([...villageInitialState]);
|
||
|
||
const marketingActions = [
|
||
(sq) => { sq[0] = '🏠'; return sq; },
|
||
(sq) => { sq[1] = '🌱'; return sq; },
|
||
(sq) => { sq[2] = '💧'; return sq; },
|
||
(sq) => { sq[3] = '🪚'; return sq; },
|
||
(sq) => { sq[4] = ''; return sq; }, // Clear forest
|
||
(sq) => { sq[4] = '⛏️'; return sq; }, // Build quarry
|
||
(sq) => { sq[5] = ''; return sq; }, // Removes stone
|
||
(sq) => { sq[5] = '🌱'; return sq; }, // Adds field
|
||
];
|
||
|
||
const advanceMarketingDemo = () => {
|
||
if (isHabitCompleting.value) return;
|
||
|
||
// Handle reset click
|
||
if (marketingStep.value >= marketingActions.length) {
|
||
isHabitCompleting.value = true;
|
||
setTimeout(() => {
|
||
marketingStep.value = 0;
|
||
showVillageArrow.value = false;
|
||
villageSquares.value = [...villageInitialState];
|
||
currentHabitText.value = demoHabits.value[0];
|
||
isHabitCompleting.value = false;
|
||
}, 400);
|
||
return;
|
||
}
|
||
|
||
isHabitCompleting.value = true;
|
||
|
||
if (marketingStep.value === 0) {
|
||
showVillageArrow.value = true;
|
||
}
|
||
|
||
// Apply current action
|
||
const action = marketingActions[marketingStep.value];
|
||
villageSquares.value = action([...villageSquares.value]);
|
||
|
||
// Set up next state
|
||
setTimeout(() => {
|
||
marketingStep.value++;
|
||
currentHabitText.value = demoHabits.value[marketingStep.value];
|
||
isHabitCompleting.value = false;
|
||
}, 400);
|
||
};
|
||
|
||
|
||
// --- Actions & UI State ---
|
||
const isSubmittingHabit = ref(false);
|
||
const explodingHabitId = ref(null);
|
||
|
||
const completeHabit = async (habitId) => { // Removed event param since it's handled by HabitCard
|
||
if (isSubmittingHabit.value) return;
|
||
isSubmittingHabit.value = true;
|
||
|
||
try {
|
||
const response = await api(`/api/habits/${habitId}/complete`, { method: 'POST' });
|
||
if (updateUser && response) {
|
||
updateUser({
|
||
coins: response.updatedCoins,
|
||
exp: response.updatedExp,
|
||
});
|
||
}
|
||
|
||
const habit = habits.value.find(h => h.id === habitId);
|
||
if (habit) {
|
||
// Optimistically update the completions. This assumes the API call is successful.
|
||
if (!habit.completions) {
|
||
habit.completions = [];
|
||
}
|
||
habit.completions.push({
|
||
id: Math.random(), // Temporary ID for reactivity
|
||
habitId: habitId,
|
||
date: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
explodingHabitId.value = habitId;
|
||
setTimeout(() => {
|
||
explodingHabitId.value = null;
|
||
}, 1000);
|
||
|
||
} catch (err) {
|
||
alert(err.data?.message || 'Failed to complete habit.');
|
||
} finally {
|
||
isSubmittingHabit.value = false;
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dashboard-content, .onboarding-container {
|
||
text-align: center;
|
||
}
|
||
|
||
.welcome-content {
|
||
padding: 80px 24px;
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 1.25rem; /* 20px */
|
||
color: var(--text-color-light);
|
||
max-width: 480px;
|
||
margin: 0 auto 32px auto;
|
||
}
|
||
|
||
.streak-section {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin: 24px 0 32px 0;
|
||
}
|
||
|
||
.streak-card {
|
||
background: #f8f9fa;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
width: 110px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.streak-card h2 {
|
||
margin: 0 0 5px 0;
|
||
font-size: 2em;
|
||
color: var(--text-color-light);
|
||
}
|
||
|
||
.streak-card p {
|
||
margin: 0;
|
||
font-size: 0.9em;
|
||
color: var(--text-color-light);
|
||
}
|
||
|
||
.active-streak {
|
||
border-color: var(--primary-color);
|
||
background-color: #fff;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.active-streak h2 {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.active-streak p {
|
||
color: var(--text-color);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.habits-section {
|
||
margin: 40px 0;
|
||
}
|
||
|
||
.auth-buttons {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 16px;
|
||
margin-top: 32px;
|
||
}
|
||
|
||
/* --- Marketing Demo --- */
|
||
.marketing-divider {
|
||
height: 1px;
|
||
width: 100px;
|
||
background-color: var(--border-color);
|
||
margin: 80px auto;
|
||
}
|
||
|
||
.marketing-container h3 {
|
||
font-size: 1.75rem; /* 28px */
|
||
margin-bottom: 48px;
|
||
}
|
||
|
||
.demo-habit-section, .demo-village-section {
|
||
position: relative;
|
||
max-width: 500px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.demo-habit-section {
|
||
margin-bottom: 64px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 60px;
|
||
}
|
||
|
||
.demo-habit-card {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 24px;
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
border: 1px solid var(--border-color);
|
||
width: 320px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.07);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.demo-habit-card span {
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.demo-habit-card span.completed {
|
||
text-decoration: line-through;
|
||
color: var(--text-color-light);
|
||
font-style: italic;
|
||
}
|
||
|
||
.demo-habit-card .btn-success {
|
||
background-color: var(--secondary-color);
|
||
border-color: var(--secondary-color);
|
||
color: #fff;
|
||
}
|
||
.demo-habit-card .btn-success:hover {
|
||
background-color: var(--secondary-color-hover);
|
||
border-color: var(--secondary-color-hover);
|
||
}
|
||
|
||
|
||
.arrow-with-text {
|
||
font-size: 1rem;
|
||
color: var(--danger-color-hover);
|
||
font-style: italic;
|
||
}
|
||
|
||
.arrow-with-text.to-button {
|
||
margin-right: 16px;
|
||
order: -1;
|
||
}
|
||
|
||
.arrow-with-text.to-village {
|
||
margin-bottom: 16px;
|
||
opacity: 0;
|
||
transition: opacity 0.5s ease-in-out;
|
||
font-weight: 600;
|
||
font-size: 1.1rem;
|
||
}
|
||
.arrow-with-text.to-village.visible {
|
||
opacity: 1;
|
||
}
|
||
|
||
.demo-village-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
max-width: 324px; /* 3 * 100 + 2 * 12 */
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.village-square-demo {
|
||
width: 100px;
|
||
height: 100px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border-radius: 16px;
|
||
transition: all 0.3s ease;
|
||
color: var(--text-color);
|
||
position: relative;
|
||
}
|
||
|
||
.village-square-demo .emoji-content {
|
||
font-size: 2.8em;
|
||
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
}
|
||
|
||
.village-square-demo.empty {
|
||
background-color: rgba(255,255,255,0.5);
|
||
border: 2px dashed var(--border-color);
|
||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.04);
|
||
}
|
||
|
||
.village-square-demo.initial-obstacle {
|
||
background-color: #f0e9d6;
|
||
border: 1px solid #e0d9c6;
|
||
}
|
||
|
||
.village-square-demo.built {
|
||
background-color: #ffffff;
|
||
border: 1px solid #fff;
|
||
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.village-square-demo.built .emoji-content {
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
/* --- Responsive Styles --- */
|
||
@media (max-width: 768px) {
|
||
.welcome-content {
|
||
padding: 40px 16px;
|
||
}
|
||
.subtitle {
|
||
font-size: 1.125rem;
|
||
}
|
||
.auth-buttons {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 0 16px;
|
||
}
|
||
.marketing-divider {
|
||
margin: 60px auto;
|
||
}
|
||
.marketing-container h3 {
|
||
font-size: 1.5rem;
|
||
margin-bottom: 32px;
|
||
}
|
||
.demo-habit-section {
|
||
flex-direction: column;
|
||
height: auto;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.arrow-with-text.to-button {
|
||
order: 0; /* Reset order */
|
||
margin: 0 0 8px 0;
|
||
text-align: center;
|
||
}
|
||
.demo-habit-card {
|
||
width: 100%;
|
||
max-width: 350px;
|
||
padding: 12px 16px;
|
||
}
|
||
.demo-village-grid {
|
||
max-width: 100%;
|
||
gap: 8px;
|
||
}
|
||
.village-square-demo {
|
||
width: auto;
|
||
/* height: auto; */
|
||
aspect-ratio: 1 / 1;
|
||
border-radius: 12px;
|
||
}
|
||
.village-square-demo .emoji-content {
|
||
font-size: 2.2rem;
|
||
}
|
||
}
|
||
</style> |