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

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

View File

@ -4,6 +4,13 @@ interface User {
id: string;
email: string;
nickname: string;
avatar: string | null;
coins: number;
exp: number;
soundOn: boolean;
confettiOn: boolean;
createdAt: string;
updatedAt: string;
}
export function useAuth() {
@ -17,16 +24,20 @@ export function useAuth() {
const isAuthenticated = computed(() => !!user.value);
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;
loading.value = true;
initialized.value = true;
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) {
user.value = null; // Silently set user to null
user.value = null; // Silently set user to null on 401
} finally {
loading.value = false;
initialized.value = true; // Mark as initialized after the first attempt
}
};

View File

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

View File

@ -8,7 +8,7 @@
<div v-if="error" class="error-message">{{ error }}</div>
<input v-model="newHabitName" type="text" placeholder="e.g., Read for 15 minutes" required />
<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" />
<span>{{ day }}</span>
</label>
@ -25,19 +25,9 @@
<div class="habit-info">
<h4>{{ habit.name }}</h4>
<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 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>
<p v-if="!loading.fetch && habits.length === 0">No habits yet. Add one above!</p>
</div>
@ -51,8 +41,7 @@ import { ref, onMounted } from 'vue';
interface Habit {
id: number;
name: string;
daysOfWeek: string[];
completions: { id: number; date: string }[];
daysOfWeek: number[]; // Backend returns numbers
}
// --- Composables ---
@ -61,39 +50,21 @@ const api = useApi();
// --- State ---
const habits = ref<Habit[]>([]);
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 error = ref<string | null>(null);
const loading = ref({
fetch: 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 ---
const fetchHabits = async () => {
loading.value.fetch = true;
error.value = null;
try {
const data = await api<Habit[]>('/habits');
habits.value = data;
habits.value = await api<Habit[]>('/habits');
} catch (err: any) {
console.error('Failed to fetch habits:', err);
error.value = 'Could not load habits.';
@ -109,18 +80,24 @@ const createHabit = async () => {
}
loading.value.create = true;
error.value = null;
// Convert day names to numbers
const dayNumbers = newHabitDays.value.map(dayName => dayOptions.indexOf(dayName));
try {
const newHabit = await api<Habit>('/habits', {
await api<Habit>('/habits', {
method: 'POST',
body: {
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 = '';
newHabitDays.value = [];
await fetchHabits();
} catch (err: any) {
console.error('Failed to create habit:', err);
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 ---
onMounted(fetchHabits);
</script>
@ -157,7 +114,7 @@ onMounted(fetchHabits);
.habits-container {
max-width: 600px;
margin: 0 auto;
padding-bottom: 40px; /* Space for form */
padding-bottom: 40px;
}
h3 {
@ -262,20 +219,6 @@ h3 {
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 {
color: #bf616a;
background-color: #fbe2e5;

View File

@ -1,61 +1,152 @@
<template>
<div class="dashboard">
<div v-if="!initialized">
<p>Loading session...</p>
<div v-if="isAuthenticated && user">
<h2>Today's Habits for {{ user.nickname }}</h2>
<div v-if="loading" class="loading-message">
<p>Loading habits...</p>
</div>
<div v-else-if="isAuthenticated && user">
<h2>Welcome Back, {{ user.nickname }}!</h2>
<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 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>
<!-- Redirect is handled in script setup, this part might not be seen -->
<div v-else class="empty-state">
<p>No habits scheduled for today. Great job!</p>
<NuxtLink to="/habits">Manage Habits</NuxtLink>
</div>
</div>
<div v-else>
<p>Redirecting to login...</p>
<p>Loading session...</p>
</div>
</div>
</template>
<script setup lang="ts">
const { user, isAuthenticated, initialized } = useAuth();
import { ref, computed, onMounted } from 'vue';
watchEffect(() => {
// Only redirect when the auth state has been initialized and the user is not authenticated.
if (initialized.value && !isAuthenticated.value) {
navigateTo('/login');
interface Habit {
id: number;
name: string;
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>
<style scoped>
.dashboard {
max-width: 600px;
margin: 0 auto;
text-align: center;
padding: 40px 20px;
}
h2 {
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
color: #4c566a;
}
.quick-links {
.habits-list {
display: flex;
justify-content: center;
gap: 20px;
flex-direction: column;
gap: 15px;
margin-top: 20px;
}
.quick-links a {
display: inline-block;
padding: 10px 20px;
background-color: #81a1c1;
color: white;
text-decoration: none;
.habit-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: #fff;
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;
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>

View File

@ -1,8 +1,11 @@
// /middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
// `app.vue` is responsible for the initial fetchUser call.
// This middleware just reads the state that's already present.
const { isAuthenticated } = useAuth();
const { isAuthenticated, initialized } = 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 (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
const publicRoutes = ['/login', '/register']; // Add any other public paths here
const publicRoutes = ['/login', '/register'];
if (!isAuthenticated.value && !publicRoutes.includes(to.path)) {
return navigateTo('/login', { replace: true });
}