Добавление привычек работает, в шапке отображается верная инфа пользователя

This commit is contained in:
Alexander Andreev 2026-01-03 15:19:27 +03:00
parent 055515269b
commit bab91b6448
6 changed files with 182 additions and 127 deletions

View File

@ -5,10 +5,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue';
const { fetchMe } = useAuth(); const { fetchMe } = useAuth();
// Fetch the user state once on app startup. // Fetch the user state ONLY on the client-side after the app has mounted.
// This call is not awaited to avoid blocking the render. // This ensures the browser's cookies are sent, allowing the session to persist.
// The `initialized` guard inside `useAuth` prevents multiple calls. onMounted(() => {
fetchMe(); fetchMe();
});
</script> </script>

View File

@ -4,6 +4,13 @@ interface User {
id: string; id: string;
email: string; email: string;
nickname: string; nickname: string;
avatar: string | null;
coins: number;
exp: number;
soundOn: boolean;
confettiOn: boolean;
createdAt: string;
updatedAt: string;
} }
export function useAuth() { export function useAuth() {
@ -17,16 +24,20 @@ export function useAuth() {
const isAuthenticated = computed(() => !!user.value); const isAuthenticated = computed(() => !!user.value);
const fetchMe = async () => { const fetchMe = async () => {
// This function can be called multiple times, but the logic inside
// will only run once thanks to the initialized flag.
if (initialized.value) return; if (initialized.value) return;
loading.value = true; loading.value = true;
initialized.value = true;
try { try {
user.value = await api<User>('/auth/me', { method: 'GET' }); // The backend returns the user object nested under a 'user' key.
const response = await api<{ user: User }>('/auth/me', { method: 'GET' });
user.value = response.user; // Correctly assign the nested user object
} catch (error) { } catch (error) {
user.value = null; // Silently set user to null user.value = null; // Silently set user to null on 401
} finally { } finally {
loading.value = false; loading.value = false;
initialized.value = true; // Mark as initialized after the first attempt
} }
}; };

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="default-layout"> <div class="default-layout">
<header class="app-header"> <header class="app-header" v-if="user">
<div class="stats"> <div class="stats">
<span>SmurfCoins: 120</span> <span>SmurfCoins: {{ user.coins }}</span>
<span>EXP: 850</span> <span>EXP: {{ user.exp }}</span>
</div> </div>
<div class="user-info"> <div class="user-info">
<span>Lvl 5</span> <!-- Level can be calculated later -->
<span>Smurfette</span> <span>{{ user.nickname }}</span>
</div> </div>
</header> </header>
@ -23,6 +23,10 @@
</div> </div>
</template> </template>
<script setup lang="ts">
const { user } = useAuth();
</script>
<style scoped> <style scoped>
.default-layout { .default-layout {
display: flex; display: flex;

View File

@ -8,7 +8,7 @@
<div v-if="error" class="error-message">{{ error }}</div> <div v-if="error" class="error-message">{{ error }}</div>
<input v-model="newHabitName" type="text" placeholder="e.g., Read for 15 minutes" required /> <input v-model="newHabitName" type="text" placeholder="e.g., Read for 15 minutes" required />
<div class="days-selector"> <div class="days-selector">
<label v-for="day in daysOfWeek" :key="day" class="day-label"> <label v-for="day in dayOptions" :key="day" class="day-label">
<input type="checkbox" :value="day" v-model="newHabitDays" /> <input type="checkbox" :value="day" v-model="newHabitDays" />
<span>{{ day }}</span> <span>{{ day }}</span>
</label> </label>
@ -25,19 +25,9 @@
<div class="habit-info"> <div class="habit-info">
<h4>{{ habit.name }}</h4> <h4>{{ habit.name }}</h4>
<div class="habit-days"> <div class="habit-days">
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ day }}</span> <span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ dayMap[day] }}</span>
</div> </div>
</div> </div>
<div class="habit-actions">
<button
v-if="isActiveToday(habit) && !isCompleteToday(habit)"
@click="completeHabit(habit.id)"
:disabled="loading.complete === habit.id"
class="complete-btn">
{{ loading.complete === habit.id ? '...' : 'Complete' }}
</button>
<span v-if="isCompleteToday(habit)" class="completed-text">Done! </span>
</div>
</div> </div>
<p v-if="!loading.fetch && habits.length === 0">No habits yet. Add one above!</p> <p v-if="!loading.fetch && habits.length === 0">No habits yet. Add one above!</p>
</div> </div>
@ -51,8 +41,7 @@ import { ref, onMounted } from 'vue';
interface Habit { interface Habit {
id: number; id: number;
name: string; name: string;
daysOfWeek: string[]; daysOfWeek: number[]; // Backend returns numbers
completions: { id: number; date: string }[];
} }
// --- Composables --- // --- Composables ---
@ -61,39 +50,21 @@ const api = useApi();
// --- State --- // --- State ---
const habits = ref<Habit[]>([]); const habits = ref<Habit[]>([]);
const newHabitName = ref(''); const newHabitName = ref('');
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const dayOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const dayMap: { [key: number]: string } = { 0: 'Mon', 1: 'Tue', 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' };
const newHabitDays = ref<string[]>([]); const newHabitDays = ref<string[]>([]);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const loading = ref({ const loading = ref({
fetch: false, fetch: false,
create: false, create: false,
complete: null as number | null,
}); });
// --- Helper Functions ---
const getTodayString = () => new Date().toISOString().split('T')[0];
const getTodayDay = () => {
const dayIndex = new Date().getDay(); // Sunday is 0
return daysOfWeek[(dayIndex + 6) % 7]; // Adjust to Mon = 0, Sun = 6 -> then map to our array
};
const isCompleteToday = (habit: Habit) => {
const today = getTodayString();
return habit.completions.some(c => c.date.startsWith(today));
};
const isActiveToday = (habit: Habit) => {
const today = getTodayDay();
return habit.daysOfWeek.includes(today);
};
// --- API Functions --- // --- API Functions ---
const fetchHabits = async () => { const fetchHabits = async () => {
loading.value.fetch = true; loading.value.fetch = true;
error.value = null; error.value = null;
try { try {
const data = await api<Habit[]>('/habits'); habits.value = await api<Habit[]>('/habits');
habits.value = data;
} catch (err: any) { } catch (err: any) {
console.error('Failed to fetch habits:', err); console.error('Failed to fetch habits:', err);
error.value = 'Could not load habits.'; error.value = 'Could not load habits.';
@ -109,18 +80,24 @@ const createHabit = async () => {
} }
loading.value.create = true; loading.value.create = true;
error.value = null; error.value = null;
// Convert day names to numbers
const dayNumbers = newHabitDays.value.map(dayName => dayOptions.indexOf(dayName));
try { try {
const newHabit = await api<Habit>('/habits', { await api<Habit>('/habits', {
method: 'POST', method: 'POST',
body: { body: {
name: newHabitName.value, name: newHabitName.value,
daysOfWeek: newHabitDays.value, daysOfWeek: dayNumbers,
}, },
}); });
habits.value.push(newHabit); // Optimistic update
// Reset form // Clear form and re-fetch the list from the server
newHabitName.value = ''; newHabitName.value = '';
newHabitDays.value = []; newHabitDays.value = [];
await fetchHabits();
} catch (err: any) { } catch (err: any) {
console.error('Failed to create habit:', err); console.error('Failed to create habit:', err);
error.value = err.data?.message || 'Could not create habit.'; error.value = err.data?.message || 'Could not create habit.';
@ -129,26 +106,6 @@ const createHabit = async () => {
} }
}; };
const completeHabit = async (habitId: number) => {
loading.value.complete = habitId;
error.value = null;
try {
const completion = await api<{ id: number; date: string }>(`/habits/${habitId}/complete`, {
method: 'POST',
});
// Optimistic update
const habit = habits.value.find(h => h.id === habitId);
if (habit) {
habit.completions.push(completion);
}
} catch (err: any) {
console.error(`Failed to complete habit ${habitId}:`, err);
error.value = err.data?.message || 'Could not complete habit.';
} finally {
loading.value.complete = null;
}
};
// --- Lifecycle Hooks --- // --- Lifecycle Hooks ---
onMounted(fetchHabits); onMounted(fetchHabits);
</script> </script>
@ -157,7 +114,7 @@ onMounted(fetchHabits);
.habits-container { .habits-container {
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
padding-bottom: 40px; /* Space for form */ padding-bottom: 40px;
} }
h3 { h3 {
@ -262,20 +219,6 @@ h3 {
font-size: 0.8em; font-size: 0.8em;
} }
.complete-btn {
padding: 8px 12px;
border: none;
border-radius: 5px;
background-color: #88c0d0;
color: #2e3440;
cursor: pointer;
}
.completed-text {
color: #a3be8c;
font-weight: bold;
}
.error-message { .error-message {
color: #bf616a; color: #bf616a;
background-color: #fbe2e5; background-color: #fbe2e5;

View File

@ -1,61 +1,152 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<div v-if="!initialized"> <div v-if="isAuthenticated && user">
<p>Loading session...</p> <h2>Today's Habits for {{ user.nickname }}</h2>
</div>
<div v-else-if="isAuthenticated && user"> <div v-if="loading" class="loading-message">
<h2>Welcome Back, {{ user.nickname }}!</h2> <p>Loading habits...</p>
<p>Your village and habits await.</p>
<div class="quick-links">
<NuxtLink to="/village">Go to Village</NuxtLink>
<NuxtLink to="/habits">Check Habits</NuxtLink>
</div> </div>
<div v-else-if="todayHabits.length > 0" class="habits-list">
<div v-for="habit in todayHabits" :key="habit.id" class="habit-card">
<span class="habit-name">{{ habit.name }}</span>
<button
v-if="!isCompleteToday(habit)"
@click="completeHabit(habit.id)"
:disabled="completing === habit.id"
class="complete-btn"
>
{{ completing === habit.id ? '...' : 'Complete' }}
</button>
<span v-else class="completed-text"> Done!</span>
</div>
</div>
<div v-else class="empty-state">
<p>No habits scheduled for today. Great job!</p>
<NuxtLink to="/habits">Manage Habits</NuxtLink>
</div>
</div> </div>
<!-- Redirect is handled in script setup, this part might not be seen -->
<div v-else> <div v-else>
<p>Redirecting to login...</p> <p>Loading session...</p>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { user, isAuthenticated, initialized } = useAuth(); import { ref, computed, onMounted } from 'vue';
watchEffect(() => { interface Habit {
// Only redirect when the auth state has been initialized and the user is not authenticated. id: number;
if (initialized.value && !isAuthenticated.value) { name: string;
navigateTo('/login'); daysOfWeek: number[];
completions: { id: number; date: string }[];
}
const { user, isAuthenticated } = useAuth();
const api = useApi();
const allHabits = ref<Habit[]>([]);
const loading = ref(true);
const completing = ref<number | null>(null);
const dayMap = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const todayIndex = new Date().getDay(); // 0 for Sunday, 1 for Monday...
const todayHabits = computed(() => {
return allHabits.value.filter(h => h.daysOfWeek.includes(todayIndex));
});
const getTodayString = () => new Date().toISOString().split('T')[0];
const isCompleteToday = (habit: Habit) => {
const today = getTodayString();
return habit.completions.some(c => c.date.startsWith(today));
};
const fetchHabits = async () => {
loading.value = true;
try {
allHabits.value = await api<Habit[]>('/habits');
} catch (error) {
console.error("Failed to fetch habits:", error);
allHabits.value = []; // Clear habits on error
} finally {
loading.value = false;
} }
};
const completeHabit = async (habitId: number) => {
completing.value = habitId;
try {
await api(`/habits/${habitId}/complete`, { method: 'POST' });
await fetchHabits(); // Re-fetch the list to update UI
} catch (error) {
console.error(`Failed to complete habit ${habitId}:`, error);
} finally {
completing.value = null;
}
};
onMounted(() => {
// We only fetch habits if the user is authenticated.
// The redirect logic is handled by the middleware/layout now.
if (isAuthenticated.value) {
fetchHabits();
}
// Watch for auth state changes to fetch habits after login
watch(isAuthenticated, (isAuth) => {
if (isAuth && allHabits.value.length === 0) {
fetchHabits();
}
});
}); });
</script> </script>
<style scoped> <style scoped>
.dashboard { .dashboard {
max-width: 600px;
margin: 0 auto;
text-align: center; text-align: center;
padding: 40px 20px;
} }
.habits-list {
h2 {
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
color: #4c566a;
}
.quick-links {
display: flex; display: flex;
justify-content: center; flex-direction: column;
gap: 20px; gap: 15px;
margin-top: 20px;
} }
.habit-card {
.quick-links a { display: flex;
display: inline-block; justify-content: space-between;
padding: 10px 20px; align-items: center;
background-color: #81a1c1; padding: 15px;
color: white; background-color: #fff;
text-decoration: none; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
text-align: left;
}
.habit-name {
font-size: 1.1em;
}
.complete-btn {
padding: 8px 12px;
border: none;
border-radius: 5px; border-radius: 5px;
background-color: #88c0d0;
color: #2e3440;
cursor: pointer;
}
.completed-text {
color: #a3be8c;
font-weight: bold;
}
.empty-state {
margin-top: 40px;
color: #666;
}
.empty-state a {
margin-top: 10px;
display: inline-block;
} }
</style> </style>

View File

@ -1,8 +1,11 @@
// /middleware/auth.ts // /middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => { export default defineNuxtRouteMiddleware((to) => {
// `app.vue` is responsible for the initial fetchUser call. const { isAuthenticated, initialized } = useAuth();
// This middleware just reads the state that's already present.
const { isAuthenticated } = useAuth(); // Do not run middleware until auth state is initialized on client-side
if (!initialized.value) {
return;
}
// if the user is authenticated and tries to access /login, redirect to home // if the user is authenticated and tries to access /login, redirect to home
if (isAuthenticated.value && to.path === '/login') { if (isAuthenticated.value && to.path === '/login') {
@ -10,7 +13,7 @@ export default defineNuxtRouteMiddleware((to) => {
} }
// if the user is not authenticated and tries to access any page other than public routes, redirect to /login // if the user is not authenticated and tries to access any page other than public routes, redirect to /login
const publicRoutes = ['/login', '/register']; // Add any other public paths here const publicRoutes = ['/login', '/register'];
if (!isAuthenticated.value && !publicRoutes.includes(to.path)) { if (!isAuthenticated.value && !publicRoutes.includes(to.path)) {
return navigateTo('/login', { replace: true }); return navigateTo('/login', { replace: true });
} }