регистрация, авторизация ок. стартовый экран и добавление привычек пока без функционала, но роуты работают
This commit is contained in:
parent
367aef0d7a
commit
055515269b
|
|
@ -1,99 +1,63 @@
|
|||
// /composables/useAuth.ts
|
||||
|
||||
// Define User interface
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for authentication management.
|
||||
* All state is managed by Nuxt's useState to ensure it's shared and SSR-safe.
|
||||
*/
|
||||
export function useAuth() {
|
||||
// --- State ---
|
||||
// All state is defined here, inside the composable function.
|
||||
// Nuxt's useState ensures this state is a singleton across the app.
|
||||
// All Nuxt composables that require instance access MUST be called inside the setup function.
|
||||
// useState is how we create shared, SSR-safe state in Nuxt.
|
||||
const user = useState<User | null>('user', () => null);
|
||||
const loading = useState('auth_loading', () => false);
|
||||
const initialized = useState('auth_initialized', () => false);
|
||||
const loading = ref(false); // This is a local, non-shared loading ref for fetchMe's internal use
|
||||
|
||||
// --- Composables ---
|
||||
const api = useApi();
|
||||
|
||||
// --- Computed Properties ---
|
||||
const isAuthenticated = computed(() => !!user.value);
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
/**
|
||||
* Fetches the current user from the backend.
|
||||
* Runs only once, guarded by the 'initialized' state.
|
||||
*/
|
||||
const fetchMe = async () => {
|
||||
if (initialized.value) {
|
||||
return;
|
||||
}
|
||||
if (initialized.value) return;
|
||||
|
||||
loading.value = true;
|
||||
initialized.value = true;
|
||||
try {
|
||||
const fetchedUser = await api<User>('/auth/me', { method: 'GET' });
|
||||
user.value = fetchedUser;
|
||||
user.value = await api<User>('/auth/me', { method: 'GET' });
|
||||
} catch (error) {
|
||||
user.value = null; // Silently handle 401s or other errors
|
||||
user.value = null; // Silently set user to null
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs the user in and fetches their data on success.
|
||||
*/
|
||||
const login = async (email, password) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// The calling component is responsible for its own loading state.
|
||||
// This function just performs the action.
|
||||
await api('/auth/login', {
|
||||
method: 'POST',
|
||||
body: { email, password },
|
||||
});
|
||||
|
||||
// We must re-fetch the user after logging in.
|
||||
// Resetting 'initialized' allows fetchMe to run again.
|
||||
// After a successful login, allow a re-fetch of the user state.
|
||||
initialized.value = false;
|
||||
await fetchMe();
|
||||
|
||||
await navigateTo('/');
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error; // Re-throw to allow the UI to handle it
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs the user out.
|
||||
*/
|
||||
const logout = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await api('/auth/logout', { method: 'POST' });
|
||||
} catch (error) {
|
||||
console.error('Logout API call failed, proceeding with client-side logout:', error);
|
||||
} finally {
|
||||
// Always clear state and redirect, regardless of API call success.
|
||||
user.value = null;
|
||||
initialized.value = false; // Allow re-fetch on next app load/login
|
||||
loading.value = false;
|
||||
initialized.value = false;
|
||||
await navigateTo('/login');
|
||||
}
|
||||
};
|
||||
|
||||
// Expose the state and methods.
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
isAuthenticated,
|
||||
initialized,
|
||||
fetchMe,
|
||||
login,
|
||||
logout,
|
||||
|
|
|
|||
|
|
@ -1,45 +1,163 @@
|
|||
<template>
|
||||
<div class="habits-container">
|
||||
<h3>My Habits</h3>
|
||||
|
||||
<!-- Create Habit Form -->
|
||||
<form @submit.prevent="createHabit" class="create-habit-form">
|
||||
<h4>Create a New Habit</h4>
|
||||
<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">
|
||||
<input type="checkbox" :value="day" v-model="newHabitDays" />
|
||||
<span>{{ day }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" :disabled="loading.create">
|
||||
{{ loading.create ? 'Adding...' : 'Add Habit' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Habits List -->
|
||||
<div class="habits-list">
|
||||
<!-- Mock Habit Card 1 -->
|
||||
<div class="habit-card">
|
||||
<p v-if="loading.fetch">Loading habits...</p>
|
||||
<div v-for="habit in habits" :key="habit.id" class="habit-card">
|
||||
<div class="habit-info">
|
||||
<h4>Practice Nuxt</h4>
|
||||
<p>Active: Mon, Wed, Fri</p>
|
||||
<h4>{{ habit.name }}</h4>
|
||||
<div class="habit-days">
|
||||
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ day }}</span>
|
||||
</div>
|
||||
<button class="complete-btn">Complete</button>
|
||||
</div>
|
||||
|
||||
<!-- Mock Habit Card 2 -->
|
||||
<div class="habit-card">
|
||||
<div class="habit-info">
|
||||
<h4>Walk 5,000 steps</h4>
|
||||
<p>Active: Everyday</p>
|
||||
<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>
|
||||
<button class="complete-btn">Complete</button>
|
||||
</div>
|
||||
|
||||
<!-- Mock Habit Card 3 -->
|
||||
<div class="habit-card">
|
||||
<div class="habit-info">
|
||||
<h4>Read a chapter</h4>
|
||||
<p>Active: Tue, Thu, Sat, Sun</p>
|
||||
</div>
|
||||
<button class="complete-btn completed">Done</button>
|
||||
</div>
|
||||
<p v-if="!loading.fetch && habits.length === 0">No habits yet. Add one above!</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// No logic, just visual placeholders
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
// --- Type Definitions ---
|
||||
interface Habit {
|
||||
id: number;
|
||||
name: string;
|
||||
daysOfWeek: string[];
|
||||
completions: { id: number; date: string }[];
|
||||
}
|
||||
|
||||
// --- Composables ---
|
||||
const api = useApi();
|
||||
|
||||
// --- State ---
|
||||
const habits = ref<Habit[]>([]);
|
||||
const newHabitName = ref('');
|
||||
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', '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;
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch habits:', err);
|
||||
error.value = 'Could not load habits.';
|
||||
} finally {
|
||||
loading.value.fetch = false;
|
||||
}
|
||||
};
|
||||
|
||||
const createHabit = async () => {
|
||||
if (!newHabitName.value || newHabitDays.value.length === 0) {
|
||||
error.value = 'Please provide a name and select at least one day.';
|
||||
return;
|
||||
}
|
||||
loading.value.create = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const newHabit = await api<Habit>('/habits', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: newHabitName.value,
|
||||
daysOfWeek: newHabitDays.value,
|
||||
},
|
||||
});
|
||||
habits.value.push(newHabit); // Optimistic update
|
||||
// Reset form
|
||||
newHabitName.value = '';
|
||||
newHabitDays.value = [];
|
||||
} catch (err: any) {
|
||||
console.error('Failed to create habit:', err);
|
||||
error.value = err.data?.message || 'Could not create habit.';
|
||||
} finally {
|
||||
loading.value.create = false;
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.habits-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 40px; /* Space for form */
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
|
@ -47,6 +165,70 @@ h3 {
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Create Form */
|
||||
.create-habit-form {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.create-habit-form h4 {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.create-habit-form input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.days-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day-label input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.day-label span {
|
||||
display: inline-block;
|
||||
width: 35px;
|
||||
line-height: 35px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 50%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.day-label input:checked + span {
|
||||
background-color: #81a1c1;
|
||||
color: white;
|
||||
border-color: #81a1c1;
|
||||
}
|
||||
|
||||
.create-habit-form button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #5e81ac;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Habits List */
|
||||
.habits-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -64,14 +246,20 @@ h3 {
|
|||
}
|
||||
|
||||
.habit-info h4 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1.1em;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.habit-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
.habit-days {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.day-chip {
|
||||
background-color: #eceff4;
|
||||
color: #4c566a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.complete-btn {
|
||||
|
|
@ -79,12 +267,21 @@ h3 {
|
|||
border: none;
|
||||
border-radius: 5px;
|
||||
background-color: #88c0d0;
|
||||
color: white;
|
||||
color: #2e3440;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.complete-btn.completed {
|
||||
background-color: #a3be8c;
|
||||
cursor: not-allowed;
|
||||
.completed-text {
|
||||
color: #a3be8c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #bf616a;
|
||||
background-color: #fbe2e5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<div v-if="isAuthenticated && user">
|
||||
<div v-if="!initialized">
|
||||
<p>Loading session...</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">
|
||||
|
|
@ -8,21 +11,19 @@
|
|||
<NuxtLink to="/habits">Check Habits</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Redirect is handled in script setup, this part might not be seen -->
|
||||
<div v-else>
|
||||
<!-- You can show a loading indicator here if you want -->
|
||||
<p>Loading...</p>
|
||||
<p>Redirecting to login...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { user, isAuthenticated, initialized } = useAuth();
|
||||
|
||||
watchEffect(() => {
|
||||
// Wait until the initial fetch is done and then decide.
|
||||
// isAuthenticated becomes false if the fetch fails (e.g., 401)
|
||||
// or true if it succeeds. We redirect only when we are sure.
|
||||
if (process.client && isAuthenticated.value === false) {
|
||||
// Only redirect when the auth state has been initialized and the user is not authenticated.
|
||||
if (initialized.value && !isAuthenticated.value) {
|
||||
navigateTo('/login');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,18 +24,23 @@ definePageMeta({
|
|||
layout: 'login',
|
||||
});
|
||||
|
||||
const { login, loading } = useAuth();
|
||||
const { login } = useAuth();
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const error = ref<string | null>(null);
|
||||
const loading = ref(false); // This is the local loading state
|
||||
|
||||
const handleLogin = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await login(email.value, password.value);
|
||||
await navigateTo('/'); // Explicitly navigate on success
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
error.value = err.data?.message || 'Login failed. Please check your credentials.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user