изменения по стилям приложения
This commit is contained in:
parent
8e1d026fd4
commit
5f8dc428be
17
app/app.vue
17
app/app.vue
|
|
@ -19,19 +19,4 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style src="../assets/css/main.css"></style>
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
background-color: #f4f4f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
font-size: 1.5em;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
37
app/composables/useVillageHelpers.ts
Normal file
37
app/composables/useVillageHelpers.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
export const useVillageHelpers = () => {
|
||||||
|
/**
|
||||||
|
* Converts numeric coordinates to a chess-like format (e.g., 0,0 -> A7).
|
||||||
|
* The grid is 5 columns (A-E) and 7 rows (1-7).
|
||||||
|
* Rows are numbered from bottom to top, so y=6 is row '1'.
|
||||||
|
* @param x The column index (0-4).
|
||||||
|
* @param y The row index (0-6).
|
||||||
|
*/
|
||||||
|
const formatCoordinates = (x: number, y: number): string => {
|
||||||
|
const col = String.fromCharCode('A'.charCodeAt(0) + x);
|
||||||
|
const row = 7 - y;
|
||||||
|
return `${col}${row}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds and replaces all occurrences of numeric coordinates like (x, y)
|
||||||
|
* in a string with the chess-like format.
|
||||||
|
* @param message The message string.
|
||||||
|
*/
|
||||||
|
const formatMessageCoordinates = (message: string): string => {
|
||||||
|
if (!message) return '';
|
||||||
|
// Regex to find coordinates like (1, 2)
|
||||||
|
return message.replace(/\((\d+), (\d+)\)/g, (match, xStr, yStr) => {
|
||||||
|
const x = parseInt(xStr, 10);
|
||||||
|
const y = parseInt(yStr, 10);
|
||||||
|
if (!isNaN(x) && !isNaN(y)) {
|
||||||
|
return formatCoordinates(x, y);
|
||||||
|
}
|
||||||
|
return match; // Return original if parsing fails
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatCoordinates,
|
||||||
|
formatMessageCoordinates,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<span>{{ user.nickname }}</span>
|
<span>{{ user.nickname }}</span>
|
||||||
<span>💰 {{ displayedCoins }}</span>
|
<span>💰 {{ displayedCoins }}</span>
|
||||||
<span>✨ {{ displayedExp }}</span>
|
<span>✨ {{ displayedExp }}</span>
|
||||||
<button @click="handleLogout" class="logout-button">Logout</button>
|
<button @click="handleLogout" class="btn btn-danger btn-sm">Выйти</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -94,43 +94,31 @@ watch(() => user.value?.exp, (newExp, oldExp) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
background-color: #5a4b3a; /* Dark earthy brown */
|
background-color: var(--container-bg-color);
|
||||||
padding: 10px 15px;
|
padding: 10px 20px;
|
||||||
border-bottom: 2px solid #4a3b2a; /* Darker brown border */
|
border-bottom: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end; /* Align user info to the right */
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #f0ead6; /* Creamy white text */
|
color: var(--text-color);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info-top {
|
.user-info-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 20px;
|
||||||
font-size: 0.9em;
|
font-size: 0.95em;
|
||||||
}
|
font-weight: 500;
|
||||||
|
|
||||||
.logout-button {
|
|
||||||
background-color: #a34a2a; /* Burnt orange/reddish brown */
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #7b3b22;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8em;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button:hover {
|
|
||||||
background-color: #8e3f22;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding-bottom: 60px; /* Space for bottom nav */
|
padding-bottom: 70px; /* Space for bottom nav */
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
|
|
@ -138,12 +126,12 @@ watch(() => user.value?.exp, (newExp, oldExp) => {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #5a4b3a; /* Dark earthy brown */
|
background-color: var(--container-bg-color);
|
||||||
border-top: 2px solid #4a3b2a; /* Darker brown border */
|
border-top: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2); /* Slightly darker shadow */
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,22 +140,23 @@ watch(() => user.value?.exp, (newExp, oldExp) => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #f0ead6; /* Creamy white text */
|
color: var(--text-color-light);
|
||||||
font-size: 0.7em;
|
font-size: 0.75em;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
color: #ffcc00; /* Gold/yellow on hover */
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item .icon {
|
.nav-item .icon {
|
||||||
font-size: 1.5em;
|
font-size: 1.8em;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.router-link-active {
|
.nav-item.router-link-exact-active {
|
||||||
color: #ffcc00; /* Gold/yellow for active link */
|
color: var(--primary-color);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,69 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="habits-container">
|
<div class="page-container">
|
||||||
<h3>Мои Привычки</h3>
|
<h1>Мои Привычки</h1>
|
||||||
|
|
||||||
<!-- Create Habit Form -->
|
<!-- Create Habit Form -->
|
||||||
<form @submit.prevent="createHabit" class="create-habit-form">
|
<div class="form-container">
|
||||||
<h4>Создать новую привычку</h4>
|
<h2>Создать новую привычку</h2>
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<form @submit.prevent="createHabit">
|
||||||
<input v-model="newHabitName" type="text" placeholder="Например, читать 15 минут" required />
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
<div class="days-selector">
|
|
||||||
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
<div class="form-group">
|
||||||
<input type="checkbox" :value="day.name" v-model="newHabitDays" />
|
<label for="newHabitName" class="form-label">Название привычки</label>
|
||||||
<span>{{ day.name }}</span>
|
<input id="newHabitName" v-model="newHabitName" type="text" placeholder="Например, читать 15 минут" class="form-control" required />
|
||||||
</label>
|
</div>
|
||||||
</div>
|
|
||||||
<button type="submit" :disabled="loading.create">
|
<div class="form-group">
|
||||||
{{ loading.create ? 'Добавляем...' : 'Добавить Привычку' }}
|
<label class="form-label">Дни недели</label>
|
||||||
</button>
|
<div class="days-selector">
|
||||||
</form>
|
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
||||||
|
<input type="checkbox" :value="day.name" v-model="newHabitDays" />
|
||||||
|
<span>{{ day.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="loading.create" class="btn btn-primary">
|
||||||
|
{{ loading.create ? 'Добавляем...' : 'Добавить Привычку' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Habits List -->
|
<!-- Habits List -->
|
||||||
<div class="habits-list">
|
<div class="habits-list">
|
||||||
|
<h2>Список привычек</h2>
|
||||||
<p v-if="loading.fetch">Загрузка привычек...</p>
|
<p v-if="loading.fetch">Загрузка привычек...</p>
|
||||||
<div v-for="habit in habits" :key="habit.id" class="habit-card">
|
<div v-for="habit in habits" :key="habit.id" class="habit-card">
|
||||||
<!-- Viewing Mode -->
|
<!-- Viewing Mode -->
|
||||||
<div v-if="editingHabitId !== habit.id" class="habit-view">
|
<div v-if="editingHabitId !== habit.id">
|
||||||
<div class="habit-info">
|
<div class="habit-info">
|
||||||
<h4>{{ habit.name }}</h4>
|
<h3>{{ habit.name }}</h3>
|
||||||
<div class="habit-days">
|
<div class="habit-days">
|
||||||
<span v-for="day in habit.daysOfWeek" :key="day" class="day-chip">{{ dayMap[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">
|
<div class="habit-actions">
|
||||||
<button @click="startEditing(habit)" class="edit-btn">Редактировать</button>
|
<button @click="startEditing(habit)" class="btn btn-secondary btn-sm">Редактировать</button>
|
||||||
<button @click="promptForDelete(habit.id)" :disabled="loading.delete" class="delete-btn">Удалить</button>
|
<button @click="promptForDelete(habit.id)" :disabled="loading.delete" class="btn btn-danger btn-sm">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Editing Mode -->
|
<!-- Editing Mode -->
|
||||||
<form v-else @submit.prevent="saveHabit(habit.id)" class="habit-edit-form">
|
<form v-else @submit.prevent="saveHabit(habit.id)" class="habit-edit-form">
|
||||||
<input v-model="editHabitName" type="text" required />
|
<div class="form-group">
|
||||||
<div class="days-selector edit-days">
|
<input v-model="editHabitName" type="text" class="form-control" required />
|
||||||
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
</div>
|
||||||
<input type="checkbox" :value="day.name" v-model="editHabitDays" />
|
<div class="form-group">
|
||||||
<span>{{ day.name }}</span>
|
<div class="days-selector edit-days">
|
||||||
</label>
|
<label v-for="day in dayOptions" :key="day.value" class="day-label">
|
||||||
|
<input type="checkbox" :value="day.name" v-model="editHabitDays" />
|
||||||
|
<span>{{ day.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button type="submit" :disabled="loading.edit" class="save-btn">Сохранить</button>
|
<button type="submit" :disabled="loading.edit" class="btn btn-primary btn-sm">Сохранить</button>
|
||||||
<button type="button" @click="cancelEditing" class="cancel-btn">Отмена</button>
|
<button type="button" @click="cancelEditing" class="btn btn-secondary btn-sm">Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,42 +259,20 @@ onMounted(fetchHabits);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.habits-container {
|
.form-container {
|
||||||
max-width: 600px;
|
background-color: var(--container-bg-color);
|
||||||
margin: 0 auto;
|
padding: 24px;
|
||||||
padding-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Create Form */
|
|
||||||
.create-habit-form {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
margin-bottom: 30px;
|
margin-bottom: 32px;
|
||||||
}
|
border: 1px solid var(--border-color);
|
||||||
|
|
||||||
.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 {
|
.days-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,117 +286,108 @@ h3 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-label span {
|
.day-label span {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
width: 35px;
|
justify-content: center;
|
||||||
line-height: 35px;
|
align-items: center;
|
||||||
border: 1px solid #ccc;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-label input:checked + span {
|
.day-label input:checked + span {
|
||||||
background-color: #81a1c1;
|
background-color: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: #81a1c1;
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-habit-form button {
|
.form-container button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
|
||||||
background-color: #5e81ac;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Habits List */
|
/* Habits List */
|
||||||
.habits-list {
|
.habits-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-card {
|
.habit-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 15px;
|
padding: 20px;
|
||||||
background-color: #fff;
|
background-color: var(--container-bg-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-view {
|
.habit-card > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-info h4 {
|
.habit-info h3 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-days {
|
.habit-days {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day-chip {
|
.day-chip {
|
||||||
background-color: #eceff4;
|
background-color: #e5e7eb;
|
||||||
color: #4c566a;
|
color: #4b5563;
|
||||||
padding: 2px 6px;
|
padding: 4px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 16px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #bf616a;
|
color: var(--danger-color);
|
||||||
background-color: #fbe2e5;
|
background-color: #fee2e2;
|
||||||
padding: 10px;
|
padding: 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 0.375rem;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Actions */
|
.habit-actions {
|
||||||
.habit-actions button {
|
display: flex;
|
||||||
padding: 5px 10px;
|
gap: 10px;
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
.edit-btn { background-color: #d8dee9; color: #4c566a; }
|
|
||||||
.delete-btn { background-color: #bf616a; color: #fff; }
|
|
||||||
|
|
||||||
/* Edit Form */
|
/* Edit Form Specific Styles */
|
||||||
.habit-edit-form {
|
.habit-edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
.habit-edit-form input[type="text"] {
|
|
||||||
width: 100%;
|
.habit-edit-form .form-group {
|
||||||
padding: 8px;
|
margin-bottom: 0;
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.edit-days {
|
|
||||||
margin-bottom: 10px;
|
.habit-edit-form .days-selector {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-actions {
|
.edit-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.edit-actions button {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.save-btn { background-color: #a3be8c; color: #fff; }
|
|
||||||
.cancel-btn { background-color: #eceff4; color: #4c566a; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="home-page">
|
<div class="page-container">
|
||||||
<div v-if="isAuthenticated && user" class="dashboard-content">
|
<div v-if="isAuthenticated && user" class="dashboard-content">
|
||||||
|
|
||||||
<h1>Ваши цели на сегодня</h1>
|
<h1>Ваши цели на сегодня</h1>
|
||||||
<p>Цели обновляются раз в сутки. Получаемые бонусы усиливаются, если посещать сайт ежедневно.</p>
|
<p class="text-color-light">Цели обновляются раз в сутки. Получаемые бонусы усиливаются, если посещать сайт ежедневно.</p>
|
||||||
|
|
||||||
<div class="streak-section">
|
<div class="streak-section">
|
||||||
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 1 }">
|
<div class="streak-card" :class="{ 'active-streak': user.dailyStreak === 1 }">
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="habits-section">
|
<div class="habits-section">
|
||||||
|
<h2>Привычки</h2>
|
||||||
<div v-if="habitsPending">Loading habits...</div>
|
<div v-if="habitsPending">Loading habits...</div>
|
||||||
<div v-else-if="habitsError">Could not load habits.</div>
|
<div v-else-if="habitsError">Could not load habits.</div>
|
||||||
<div v-else-if="habits && habits.length > 0">
|
<div v-else-if="habits && habits.length > 0">
|
||||||
|
|
@ -33,7 +34,7 @@
|
||||||
<div class="habit-action">
|
<div class="habit-action">
|
||||||
<div v-if="isScheduledForToday(habit)">
|
<div v-if="isScheduledForToday(habit)">
|
||||||
<span v-if="isCompleted(habit, today)" class="completed-text">Выполнено!</span>
|
<span v-if="isCompleted(habit, today)" class="completed-text">Выполнено!</span>
|
||||||
<button v-else @click="completeHabit(habit.id, $event)" :disabled="isSubmittingHabit">
|
<button v-else @click="completeHabit(habit.id, $event)" :disabled="isSubmittingHabit" class="btn btn-primary btn-sm">
|
||||||
Выполнить
|
Выполнить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,17 +53,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p>You have no habits yet. Go to the <NuxtLink to="/habits">My Habits</NuxtLink> page to create one.</p>
|
<p>У вас еще нет привычек. Перейдите на страницу <NuxtLink to="/habits">Мои привычки</NuxtLink>, чтобы создать их.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="welcome-content">
|
<div v-else class="welcome-content">
|
||||||
<h1>Добро пожаловать в SmurfHabits!</h1>
|
<h1>Добро пожаловать в SmurfHabits!</h1>
|
||||||
<p>Отслеживайте свои привычки и развивайте свою деревню.</p>
|
<p class="text-color-light">Отслеживайте свои привычки и развивайте свою деревню.</p>
|
||||||
<div class="auth-buttons">
|
<div class="auth-buttons">
|
||||||
<NuxtLink to="/login" class="button primary">Войти</NuxtLink>
|
<NuxtLink to="/login" class="btn btn-primary">Войти</NuxtLink>
|
||||||
<NuxtLink to="/register" class="button secondary">Зарегистрироваться</NuxtLink>
|
<NuxtLink to="/register" class="btn btn-secondary">Зарегистрироваться</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -219,185 +220,71 @@ const completeHabit = async (habitId, event) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.home-page {
|
/* Scoped styles are kept for component-specific adjustments and animations */
|
||||||
padding: 40px;
|
.dashboard-content, .welcome-content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.streak-section {
|
.streak-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px; /* Reduced gap */
|
gap: 16px;
|
||||||
margin-top: 20px; /* Added margin-top to separate from paragraph */
|
margin: 24px 0 32px 0;
|
||||||
margin-bottom: 30px; /* Slightly reduced margin-bottom */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.streak-card {
|
.streak-card {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border: 2px solid #e9ecef;
|
border: 2px solid var(--border-color);
|
||||||
border-radius: 10px; /* Slightly reduced border-radius */
|
border-radius: 12px;
|
||||||
padding: 10px 15px; /* Reduced padding */
|
padding: 16px 20px;
|
||||||
width: 100px; /* Reduced width */
|
width: 110px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
font-size: 0.9em; /* Reduced base font size */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.streak-card h2 {
|
.streak-card h2 {
|
||||||
margin: 0 0 5px 0; /* Reduced margin */
|
margin: 0 0 5px 0;
|
||||||
font-size: 1.8em; /* Reduced font size */
|
font-size: 2em;
|
||||||
color: #adb5bd;
|
color: var(--text-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.streak-card p {
|
.streak-card p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.8em; /* Reduced font size */
|
font-size: 0.9em;
|
||||||
color: #6c757d;
|
color: var(--text-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-streak {
|
.active-streak {
|
||||||
border-color: #81a1c1;
|
border-color: var(--primary-color);
|
||||||
background-color: #eceff4;
|
background-color: #f0f5ff;
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Reduced shadow */
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
transform: translateY(-3px); /* Reduced transform */
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-streak h2 {
|
.active-streak h2 {
|
||||||
color: #4c566a;
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-streak p {
|
.active-streak p {
|
||||||
color: #3b4252;
|
color: var(--text-color);
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.habits-section {
|
.habits-section {
|
||||||
margin-top: 40px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.habit-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px auto;
|
|
||||||
max-width: 800px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(7, 1fr);
|
|
||||||
gap: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-cell {
|
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-cell.completed {
|
|
||||||
background-color: #4ade80;
|
|
||||||
color: white;
|
|
||||||
border-color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-cell.missed-day {
|
|
||||||
background-color: #feecf0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-cell.scheduled-day {
|
|
||||||
border-width: 2px;
|
|
||||||
border-color: #81a1c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.future-day .day-label {
|
|
||||||
color: #adb5bd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-cell.today-highlight .day-label {
|
|
||||||
text-decoration: underline;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.day-label {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-content {
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-content h1 {
|
|
||||||
font-size: 2.5em;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-content p {
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 12px 25px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1em;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.primary {
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.primary:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary {
|
|
||||||
background-color: #6c757d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.secondary:hover {
|
|
||||||
background-color: #5a6268;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin: 40px 0;
|
margin: 40px 0;
|
||||||
}
|
}
|
||||||
.links a.button {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.links a.button:hover {
|
|
||||||
background-color: #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* New Habit Card Styles */
|
|
||||||
.habit-card {
|
.habit-card {
|
||||||
|
background: var(--container-bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 24px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -415,31 +302,78 @@ const completeHabit = async (habitId, event) => {
|
||||||
|
|
||||||
.habit-details h3 {
|
.habit-details h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.habit-schedule {
|
.habit-schedule {
|
||||||
margin: 5px 0 0 0;
|
margin: 5px 0 0 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #6c757d;
|
color: var(--text-color-light);
|
||||||
}
|
|
||||||
|
|
||||||
.habit-action button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #5e81ac;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.habit-action button:hover {
|
|
||||||
background-color: #4c566a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed-text {
|
.completed-text {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #28a745;
|
color: #16a34a; /* A nice green */
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell.completed {
|
||||||
|
background-color: #4ade80;
|
||||||
|
color: white;
|
||||||
|
border-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell.missed-day {
|
||||||
|
background-color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell.scheduled-day {
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.future-day .day-label {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell.today-highlight::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-label {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Confetti Animation */
|
/* Confetti Animation */
|
||||||
|
|
@ -451,6 +385,7 @@ const completeHabit = async (habitId, event) => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confetti-particle {
|
.confetti-particle {
|
||||||
|
|
@ -490,5 +425,4 @@ const completeHabit = async (habitId, event) => {
|
||||||
.confetti-particle:nth-child(13) { background-color: #ebcb8b; --x-end: 40px; animation-delay: 0.18s; }
|
.confetti-particle:nth-child(13) { background-color: #ebcb8b; --x-end: 40px; animation-delay: 0.18s; }
|
||||||
.confetti-particle:nth-child(14) { background-color: #81a1c1; --x-end: -40px; animation-delay: 0.22s; }
|
.confetti-particle:nth-child(14) { background-color: #81a1c1; --x-end: -40px; animation-delay: 0.22s; }
|
||||||
.confetti-particle:nth-child(15) { background-color: #b48ead; --x-end: 0px; animation-delay: 0.28s; }
|
.confetti-particle:nth-child(15) { background-color: #b48ead; --x-end: 0px; animation-delay: 0.28s; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,22 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="leaderboard-container">
|
<div class="page-container">
|
||||||
<h3>Monthly Leaderboard</h3>
|
<h1>Monthly Leaderboard</h1>
|
||||||
<div v-if="pending" class="loading">Loading leaderboard...</div>
|
<div v-if="pending" class="loading">Loading leaderboard...</div>
|
||||||
<div v-else-if="error" class="error-container">
|
<div v-else-if="error" class="error-container">
|
||||||
<p>An error occurred while fetching the leaderboard. Please try again.</p>
|
<p>An error occurred while fetching the leaderboard. Please try again.</p>
|
||||||
</div>
|
</div>
|
||||||
<ul v-else class="leaderboard-list">
|
<div v-else class="table-container">
|
||||||
<li
|
<table class="table table-striped table-hover">
|
||||||
v-for="entry in leaderboard"
|
<thead>
|
||||||
:key="entry.rank + entry.nickname"
|
<tr>
|
||||||
class="leaderboard-item"
|
<th>Rank</th>
|
||||||
:class="{ 'self': currentUser && currentUser.nickname === entry.nickname }"
|
<th>Nickname</th>
|
||||||
>
|
<th>EXP</th>
|
||||||
<span class="rank">{{ entry.rank }}.</span>
|
</tr>
|
||||||
<span class="name">{{ entry.nickname }}</span>
|
</thead>
|
||||||
<span class="exp">{{ entry.exp }} EXP</span>
|
<tbody>
|
||||||
</li>
|
<tr
|
||||||
</ul>
|
v-for="entry in leaderboard"
|
||||||
|
:key="entry.rank + entry.nickname"
|
||||||
|
:class="{ 'current-user-row': currentUser && currentUser.nickname === entry.nickname }"
|
||||||
|
>
|
||||||
|
<td>{{ entry.rank }}</td>
|
||||||
|
<td>{{ entry.nickname }}</td>
|
||||||
|
<td>{{ entry.exp }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -35,49 +45,16 @@ const leaderboard = computed(() => data.value?.leaderboard || []);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.leaderboard-container {
|
.table-container {
|
||||||
max-width: 600px;
|
overflow-x: auto;
|
||||||
margin: 0 auto;
|
}
|
||||||
|
.current-user-row > * {
|
||||||
|
background-color: #e0e7ff; /* A light blue/indigo for highlighting */
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.table-hover > tbody > tr.current-user-row:hover > * {
|
||||||
text-align: center;
|
background-color: #c7d2fe; /* A slightly darker shade for hover */
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 15px;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard-item.self {
|
|
||||||
background-color: #d8e1e9;
|
|
||||||
border: 1px solid #81a1c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rank {
|
|
||||||
font-weight: bold;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exp {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4c566a;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="auth-page">
|
<div class="auth-container">
|
||||||
<div class="auth-container">
|
<div class="page-container auth-form">
|
||||||
<h1>Login</h1>
|
<h1>Вход</h1>
|
||||||
<form @submit.prevent="handleLogin">
|
<form @submit.prevent="handleLogin">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="email" id="email" v-model="email" required />
|
<input type="email" id="email" v-model="email" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="password" class="form-label">Пароль</label>
|
||||||
<input type="password" id="password" v-model="password" required />
|
<input type="password" id="password" v-model="password" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
<button type="submit" :disabled="loading">
|
<button type="submit" :disabled="loading" class="btn btn-primary">
|
||||||
{{ loading ? 'Logging in...' : 'Login' }}
|
{{ loading ? 'Входим...' : 'Войти' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="switch-link">
|
<div class="switch-link">
|
||||||
<p>
|
<p>
|
||||||
Don't have an account?
|
Нет аккаунта?
|
||||||
<NuxtLink to="/register">Register here</NuxtLink>
|
<NuxtLink to="/register">Зарегистрироваться</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,59 +54,26 @@ definePageMeta({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-page {
|
.auth-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f0f2f5;
|
|
||||||
}
|
|
||||||
.auth-container {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
|
||||||
padding: 40px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
h1 {
|
.auth-form {
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
max-width: 420px;
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
button {
|
.auth-form button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
button:hover:not(:disabled) {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
}
|
||||||
.error-message {
|
.error-message {
|
||||||
color: red;
|
color: var(--danger-color);
|
||||||
margin-bottom: 16px;
|
background-color: #fee2e2;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.switch-link {
|
.switch-link {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="auth-page">
|
<div class="auth-container">
|
||||||
<div class="auth-container">
|
<div class="page-container auth-form">
|
||||||
<h1>Register</h1>
|
<h1>Регистрация</h1>
|
||||||
<form @submit.prevent="handleRegister">
|
<form @submit.prevent="handleRegister">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="nickname">Nickname</label>
|
<label for="nickname" class="form-label">Никнейм</label>
|
||||||
<input type="text" id="nickname" v-model="nickname" required />
|
<input type="text" id="nickname" v-model="nickname" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email</label>
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="email" id="email" v-model="email" required />
|
<input type="email" id="email" v-model="email" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password (min 8 characters)</label>
|
<label for="password" class="form-label">Пароль (минимум 8 символов)</label>
|
||||||
<input type="password" id="password" v-model="password" required />
|
<input type="password" id="password" v-model="password" class="form-control" required />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
<div v-if="successMessage" class="success-message">{{ successMessage }}</div>
|
<div v-if="successMessage" class="success-message">{{ successMessage }}</div>
|
||||||
<button type="submit" :disabled="loading">
|
<button type="submit" :disabled="loading" class="btn btn-primary">
|
||||||
{{ loading ? 'Registering...' : 'Register' }}
|
{{ loading ? 'Регистрируем...' : 'Зарегистрироваться' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="switch-link">
|
<div class="switch-link">
|
||||||
<p>
|
<p>
|
||||||
Already have an account?
|
Уже есть аккаунт?
|
||||||
<NuxtLink to="/login">Login here</NuxtLink>
|
<NuxtLink to="/login">Войти</NuxtLink>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,7 +55,7 @@ const handleRegister = async () => {
|
||||||
password: password.value,
|
password: password.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
successMessage.value = 'Registration successful! Please log in.';
|
successMessage.value = 'Регистрация прошла успешно! Пожалуйста, войдите в систему.';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('/login');
|
navigateTo('/login');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
@ -72,64 +72,34 @@ definePageMeta({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.auth-page {
|
.auth-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f0f2f5;
|
|
||||||
}
|
|
||||||
.auth-container {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
|
||||||
padding: 40px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
h1 {
|
.auth-form {
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
max-width: 420px;
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
button {
|
.auth-form button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
button:hover:not(:disabled) {
|
|
||||||
background-color: #218838;
|
|
||||||
}
|
}
|
||||||
.error-message {
|
.error-message {
|
||||||
color: red;
|
color: var(--danger-color);
|
||||||
margin-bottom: 16px;
|
background-color: #fee2e2;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.success-message {
|
.success-message {
|
||||||
color: green;
|
color: #16a34a;
|
||||||
margin-bottom: 16px;
|
background-color: #dcfce7;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.switch-link {
|
.switch-link {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="village-page">
|
<div class="page-container village-page-layout">
|
||||||
<h1>My Village</h1>
|
<h1>Моя деревня</h1>
|
||||||
|
|
||||||
<div v-if="pending" class="loading">Loading your village...</div>
|
<div v-if="pending" class="loading">Загрузка вашей деревни...</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="error-container">
|
<div v-else-if="error" class="error-container">
|
||||||
<p v-if="error.statusCode === 401">Please log in to view your village.</p>
|
<p v-if="error.statusCode === 401">Пожалуйста, войдите, чтобы увидеть свою деревню.</p>
|
||||||
<p v-else>An error occurred while fetching your village data. Please try again.</p>
|
<p v-else>Произошла ошибка при загрузке данных о деревне. Пожалуйста, попробуйте снова.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="villageData" class="village-container">
|
<div v-else-if="villageData">
|
||||||
<div class="village-grid-wrapper">
|
<div class="village-container">
|
||||||
<div class="village-grid">
|
<div class="village-grid-wrapper"> <!-- This will be the main grid container with labels -->
|
||||||
<div
|
<div class="empty-corner"></div> <!-- Top-left empty cell -->
|
||||||
v-for="tile in villageData.tiles"
|
|
||||||
:key="tile.id"
|
<div class="col-labels">
|
||||||
class="tile"
|
<div class="col-label" v-for="colLabel in ['A', 'B', 'C', 'D', 'E']" :key="colLabel">{{ colLabel }}</div>
|
||||||
:class="[tileClasses(tile), { selected: selectedTile && selectedTile.id === tile.id }]"
|
</div>
|
||||||
@click="selectTile(tile)"
|
|
||||||
>
|
<div class="row-labels">
|
||||||
<span class="tile-content">{{ getTileEmoji(tile) }}</span>
|
<div class="row-label" v-for="rowLabel in ['7', '6', '5', '4', '3', '2', '1']" :key="rowLabel">{{ rowLabel }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="village-grid">
|
||||||
|
<div
|
||||||
|
v-for="tile in villageData.tiles"
|
||||||
|
:key="tile.id"
|
||||||
|
class="tile"
|
||||||
|
:class="[tileClasses(tile), { selected: selectedTile && selectedTile.id === tile.id }]"
|
||||||
|
@click="selectTile(tile)"
|
||||||
|
>
|
||||||
|
<span class="tile-content">{{ getTileEmoji(tile) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -50,7 +62,7 @@
|
||||||
<div class="cost">
|
<div class="cost">
|
||||||
{{ action.cost }} монет
|
{{ action.cost }} монет
|
||||||
</div>
|
</div>
|
||||||
<button :disabled="!action.isEnabled || isSubmitting" @click="handleActionClick(action)">
|
<button :disabled="!action.isEnabled || isSubmitting" @click="handleActionClick(action)" class="btn btn-primary btn-sm">
|
||||||
{{ getActionLabel(action) }}
|
{{ getActionLabel(action) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,39 +75,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button @click="selectedTile = null" class="close-overlay-button">Закрыть</button>
|
<button @click="selectedTile = null" class="btn btn-secondary close-overlay-button">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-content">
|
||||||
|
<!-- Admin Panel -->
|
||||||
|
<div v-if="villageData?.user?.id === 1" class="admin-panel">
|
||||||
|
<h3>Admin Tools</h3>
|
||||||
|
<button @click="handleResetVillage" :disabled="isSubmittingAdminAction">Reset Village</button>
|
||||||
|
<button @click="handleTriggerTick" :disabled="isSubmittingAdminAction">Trigger Next Tick</button>
|
||||||
|
<button @click="handleAddCoins" :disabled="isSubmittingAdminAction">Add 1000 Coins</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Log -->
|
||||||
|
<div v-if="villageEvents?.length" class="event-log-container">
|
||||||
|
<h2>Журнал событий</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Событие</th>
|
||||||
|
<th>Монеты</th>
|
||||||
|
<th>EXP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="event in villageEvents" :key="event.id">
|
||||||
|
<td>{{ new Date(event.createdAt).toLocaleString() }}</td>
|
||||||
|
<td class="event-message">{{ formatMessageCoordinates(event.message) }}</td>
|
||||||
|
<td>{{ event.coins }}</td>
|
||||||
|
<td>{{ event.exp }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Admin Panel -->
|
|
||||||
<div v-if="villageData?.user?.id === 1" class="admin-panel">
|
|
||||||
<h3>Admin Tools</h3>
|
|
||||||
<button @click="handleResetVillage" :disabled="isSubmittingAdminAction">Reset Village</button>
|
|
||||||
<button @click="handleCompleteClearing" :disabled="isSubmittingAdminAction">Complete All Clearing</button>
|
|
||||||
<button @click="handleTriggerTick" :disabled="isSubmittingAdminAction">Trigger Next Tick</button>
|
|
||||||
</div>
|
|
||||||
<!-- Event Log -->
|
|
||||||
<div v-if="villageEvents?.length" class="event-log-container">
|
|
||||||
<h4>Activity Log</h4>
|
|
||||||
<table class="event-log-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Event</th>
|
|
||||||
<th>Coins</th>
|
|
||||||
<th>EXP</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="event in villageEvents" :key="event.id">
|
|
||||||
<td>{{ new Date(event.createdAt).toLocaleString() }}</td>
|
|
||||||
<td>{{ event.message }}</td>
|
|
||||||
<td>{{ event.coins }}</td>
|
|
||||||
<td>{{ event.exp }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -103,7 +120,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuth } from '~/composables/useAuth'; // Import useAuth
|
import { useAuth } from '~/composables/useAuth'; // Import useAuth
|
||||||
|
import { useVillageHelpers } from '~/composables/useVillageHelpers';
|
||||||
|
|
||||||
|
const { formatCoordinates, formatMessageCoordinates } = useVillageHelpers();
|
||||||
const { user, isAuthenticated, logout, updateUser } = useAuth(); // Destructure updateUser
|
const { user, isAuthenticated, logout, updateUser } = useAuth(); // Destructure updateUser
|
||||||
|
|
||||||
const { data: villageData, pending, error, refresh: refreshVillageData } = await useFetch('/api/village', {
|
const { data: villageData, pending, error, refresh: refreshVillageData } = await useFetch('/api/village', {
|
||||||
|
|
@ -189,14 +208,14 @@ const getTileTitle = (tile) => {
|
||||||
'QUARRY': 'Каменоломня',
|
'QUARRY': 'Каменоломня',
|
||||||
'WELL': 'Колодец',
|
'WELL': 'Колодец',
|
||||||
};
|
};
|
||||||
return `${buildingMap[tile.object.type] || 'Неизвестное строение'} (${tile.x}, ${tile.y})`;
|
return `${buildingMap[tile.object.type] || 'Неизвестное строение'} ${formatCoordinates(tile.x, tile.y)}`;
|
||||||
}
|
}
|
||||||
const terrainMap = {
|
const terrainMap = {
|
||||||
'BLOCKED_TREE': 'Лесной участок',
|
'BLOCKED_TREE': 'Лесной участок',
|
||||||
'BLOCKED_STONE': 'Каменистый участок',
|
'BLOCKED_STONE': 'Каменистый участок',
|
||||||
'EMPTY': 'Пустырь',
|
'EMPTY': 'Пустырь',
|
||||||
};
|
};
|
||||||
return `${terrainMap[tile.terrainType] || 'Неизвестная земля'} (${tile.x}, ${tile.y})`;
|
return `${terrainMap[tile.terrainType] || 'Неизвестная земля'} ${formatCoordinates(tile.x, tile.y)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTileDescription = (tile) => {
|
const getTileDescription = (tile) => {
|
||||||
|
|
@ -268,131 +287,206 @@ const handleActionClick = async (action) => {
|
||||||
|
|
||||||
const isSubmittingAdminAction = ref(false);
|
const isSubmittingAdminAction = ref(false);
|
||||||
|
|
||||||
async function handleAdminAction(url) {
|
async function handleAdminAction(url: string) {
|
||||||
if (isSubmittingAdminAction.value) return;
|
if (isSubmittingAdminAction.value) return;
|
||||||
isSubmittingAdminAction.value = true;
|
isSubmittingAdminAction.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await useFetch(url, { method: 'POST' });
|
// 1. Perform the requested admin action (e.g., reset, trigger tick)
|
||||||
if (error.value) {
|
const { error: actionError } = await useFetch(url, { method: 'POST' });
|
||||||
alert(error.value.data?.statusMessage || 'An admin action failed.');
|
if (actionError.value) {
|
||||||
} else {
|
// If the action itself fails, throw to stop execution
|
||||||
await Promise.all([refreshVillageData(), refreshEvents()]);
|
throw actionError.value;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to perform admin action:', e);
|
// 2. Refresh the main village data. This runs the core game logic
|
||||||
alert('An unexpected error occurred.');
|
// on the backend and gets the updated state.
|
||||||
|
await refreshVillageData();
|
||||||
|
|
||||||
|
// 3. The `villageData` ref is now updated. If it contains a user object,
|
||||||
|
// we sync it with the global auth state to update the header.
|
||||||
|
if (villageData.value?.user) {
|
||||||
|
updateUser(villageData.value.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Refresh the event log to show any new events created by the action.
|
||||||
|
await refreshEvents();
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Failed to perform admin action at ${url}:`, e);
|
||||||
|
// Use the error's data object if it exists for a more specific message
|
||||||
|
alert(e.data?.statusMessage || e.message || 'An unexpected error occurred during the admin action.');
|
||||||
} finally {
|
} finally {
|
||||||
isSubmittingAdminAction.value = false;
|
isSubmittingAdminAction.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResetVillage = () => handleAdminAction('/api/admin/village/reset');
|
const handleResetVillage = () => handleAdminAction('/api/admin/village/reset');
|
||||||
const handleCompleteClearing = () => handleAdminAction('/api/admin/village/complete-clearing');
|
|
||||||
const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-tick');
|
const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-tick');
|
||||||
|
const handleAddCoins = () => handleAdminAction('/api/admin/user/add-coins');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.village-page {
|
.village-page-layout {
|
||||||
|
--tile-size: clamp(50px, 12vw, 65px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading, .error-container {
|
.loading, .error-container {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
color: #555;
|
color: #555;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.village-container {
|
.village-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 350px;
|
padding: 0 10px; /* Add some padding on mobile */
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.village-grid-wrapper {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.village-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 60px);
|
|
||||||
grid-template-rows: repeat(7, 60px);
|
|
||||||
gap: 4px;
|
|
||||||
border: 2px solid #333;
|
|
||||||
padding: 4px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile {
|
.tile {
|
||||||
width: 60px;
|
width: var(--tile-size);
|
||||||
height: 60px;
|
height: var(--tile-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid var(--border-color);
|
||||||
background-color: #fff; /* Default white for empty */
|
border-radius: 4px;
|
||||||
|
background-color: var(--container-bg-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.tile-blocked {
|
.tile.tile-blocked {
|
||||||
background-color: #e0e0e0; /* Light gray for blocked (tree/stone) */
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.tile-object {
|
.tile.tile-object {
|
||||||
background-color: #e6ffe6; /* Light green for tiles with user objects */
|
background-color: #ecfdf5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile:hover {
|
.tile:hover {
|
||||||
background-color: #ffffe0; /* Light yellow for hover */
|
background-color: #fefce8;
|
||||||
border: 1px solid #ffcc00; /* Subtle yellow border */
|
border-color: #facc15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.selected {
|
.tile.selected {
|
||||||
border: 2px solid #007bff;
|
border: 2px solid var(--primary-color);
|
||||||
box-shadow: 0 0 10px rgba(0, 123, 255, 0.5);
|
box-shadow: 0 0 10px rgb(59 130 246 / 50%);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-content {
|
.tile-content {
|
||||||
font-size: 2em;
|
font-size: calc(var(--tile-size) * 0.4); /* Make emoji scale with tile */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles for the grid with labels */
|
||||||
|
.village-grid-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px repeat(5, var(--tile-size));
|
||||||
|
grid-template-rows: repeat(7, var(--tile-size)) 20px; /* Grid first, then labels */
|
||||||
|
gap: 4px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
padding: 4px;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow-x: auto; /* Allow horizontal scroll if needed */
|
||||||
|
overflow-y: hidden; /* Prevent vertical scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-corner {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 8; /* Position at the bottom-left */
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-labels {
|
||||||
|
grid-column: 2 / span 5;
|
||||||
|
grid-row: 8; /* Position labels at the bottom (8th row) */
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--text-color-light);
|
||||||
|
font-weight: normal; /* Muted */
|
||||||
|
opacity: 0.7; /* Muted */
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-label {
|
||||||
|
width: var(--tile-size); /* Match new tile width */
|
||||||
|
text-align: center;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-labels {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / span 7; /* Position labels on the left of the grid */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
width: 20px;
|
||||||
|
color: var(--text-color-light);
|
||||||
|
font-weight: normal; /* Muted */
|
||||||
|
opacity: 0.7; /* Muted */
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
height: var(--tile-size); /* Match new tile height */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.village-grid {
|
||||||
|
grid-column: 2 / span 5;
|
||||||
|
grid-row: 1 / span 7; /* Position grid at the top */
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, var(--tile-size));
|
||||||
|
grid-template-rows: repeat(7, var(--tile-size));
|
||||||
|
gap: 4px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Overlay and other styles */
|
||||||
|
|
||||||
.tile-overlay-backdrop {
|
.tile-overlay-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-overlay-panel {
|
.tile-overlay-panel {
|
||||||
background-color: #fff;
|
background-color: var(--background-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
border-top-left-radius: 15px;
|
border-top-left-radius: 15px;
|
||||||
border-top-right-radius: 15px;
|
border-top-right-radius: 15px;
|
||||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
|
||||||
transform: translateY(0);
|
|
||||||
transition: transform 0.3s ease-out;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 16px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
@ -401,20 +495,17 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
}
|
}
|
||||||
.tile-overlay-panel {
|
.tile-overlay-panel {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
max-height: 80vh;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-overlay-panel h2 {
|
.tile-overlay-panel h2 {
|
||||||
margin-top: 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-description {
|
.tile-description {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
color: #666;
|
color: var(--text-color-light);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,83 +515,33 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
|
|
||||||
.actions-header {
|
.actions-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.2em;
|
|
||||||
color: #444;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-item button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border: 1px solid #007bff;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1em;
|
|
||||||
transition: background-color 0.2s, opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-item button:hover:not(:disabled) {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-item button:disabled {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
color: #6c757d;
|
|
||||||
cursor: not-allowed;
|
|
||||||
border-color: #e9ecef;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.build-section {
|
.build-section {
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
|
||||||
|
|
||||||
.build-section h4 {
|
|
||||||
text-align: center;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-description {
|
.building-description {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #555;
|
color: var(--text-color-light);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-reason {
|
|
||||||
font-size: 0.8em;
|
|
||||||
color: #dc3545;
|
|
||||||
margin-top: 5px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-overlay-button {
|
.close-overlay-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 15px;
|
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
background-color: #6c757d;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-overlay-button:hover {
|
.bottom-content {
|
||||||
background-color: #5a6268;
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-panel {
|
.admin-panel {
|
||||||
|
|
@ -508,23 +549,22 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border: 2px dashed #dc3545;
|
border: 2px dashed var(--danger-color);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
max-width: 350px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-panel h3 {
|
.admin-panel h3 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
color: #dc3545;
|
color: var(--danger-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-panel button {
|
.admin-panel button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #dc3545;
|
background-color: var(--danger-color);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
@ -537,37 +577,39 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log-container {
|
.event-log-container {
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 350px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log-container h4 {
|
.event-log-container h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log-table {
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
max-height: 400px;
|
||||||
font-size: 0.8em;
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log-table th, .event-log-table td {
|
.table td, .table th {
|
||||||
border: 1px solid #ccc;
|
white-space: nowrap;
|
||||||
padding: 6px;
|
padding: 12px 15px;
|
||||||
text-align: left;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-log-table th {
|
.table .event-message {
|
||||||
background-color: #f0f0f0;
|
white-space: normal;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* New Build Card Styles */
|
|
||||||
.build-card-grid {
|
.build-card-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -576,39 +618,41 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 130px;
|
width: 140px;
|
||||||
padding: 10px;
|
padding: 16px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #f9f9f9;
|
background-color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-card:not(.disabled):hover {
|
.building-card:not(.disabled):hover {
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-card.disabled {
|
.building-card.disabled {
|
||||||
background-color: #e9ecef;
|
background-color: #f9fafb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-icon {
|
.building-icon {
|
||||||
font-size: 2em;
|
font-size: 2.5em;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-card h5 {
|
.building-card h5 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: 1em;
|
font-size: 1.05em;
|
||||||
color: #333;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-card .building-description {
|
.building-card .building-description {
|
||||||
font-size: 0.75em;
|
font-size: 0.8rem;
|
||||||
color: #666;
|
line-height: 1.5;
|
||||||
flex-grow: 1; /* Pushes footer down */
|
color: var(--text-color-light);
|
||||||
margin-bottom: 10px;
|
flex-grow: 1;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-footer {
|
.building-footer {
|
||||||
|
|
@ -616,16 +660,11 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: auto; /* Pushes footer to bottom */
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.building-footer .cost {
|
.building-footer .cost {
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.building-footer button {
|
|
||||||
padding: 5px 10px;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -635,14 +674,14 @@ const handleTriggerTick = () => handleAdminAction('/api/admin/village/trigger-ti
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(233, 236, 239, 0.8);
|
background: rgba(249, 250, 251, 0.85);
|
||||||
color: #dc3545;
|
color: var(--danger-color);
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
217
assets/css/main.css
Normal file
217
assets/css/main.css
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
/* assets/css/main.css */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #3b82f6; /* A friendly blue */
|
||||||
|
--primary-color-hover: #2563eb;
|
||||||
|
--secondary-color: #6b7280; /* A neutral gray */
|
||||||
|
--secondary-color-hover: #4b5563;
|
||||||
|
--danger-color: #ef4444; /* A soft red */
|
||||||
|
--danger-color-hover: #dc2626;
|
||||||
|
--background-color: #f9fafb; /* Very light gray for page backgrounds */
|
||||||
|
--container-bg-color: #ffffff;
|
||||||
|
--text-color: #1f2937;
|
||||||
|
--text-color-light: #6b7280;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: var(--container-bg-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem; /* 32px */
|
||||||
|
margin-bottom: 1.5rem; /* 24px */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem; /* 24px */
|
||||||
|
margin-bottom: 1rem; /* 16px */
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem; /* 20px */
|
||||||
|
margin-bottom: 0.75rem; /* 12px */
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Buttons --- */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-color-hover);
|
||||||
|
border-color: var(--primary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: var(--secondary-color-hover);
|
||||||
|
border-color: var(--secondary-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
border-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: var(--danger-color-hover);
|
||||||
|
border-color: var(--danger-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Forms --- */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: 0;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgb(59 130 246 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 2.25rem 0.75rem 1rem;
|
||||||
|
-moz-padding-start: calc(1rem - 3px);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #212529;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 1rem center;
|
||||||
|
background-size: 16px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Tables --- */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
vertical-align: top;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > :not(caption) > * > * {
|
||||||
|
padding: 0.75rem 0.75rem;
|
||||||
|
background-color: var(--container-bg-color);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > thead {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > thead > tr > th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: #f3f4f6; /* A bit darker than page background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover > tbody > tr:hover > * {
|
||||||
|
background-color: rgb(243 244 246 / 0.8);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
devtools: { enabled: true },
|
||||||
devtools: { enabled: true }
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
22
server/api/admin/user/add-coins.post.ts
Normal file
22
server/api/admin/user/add-coins.post.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { getUserIdFromSession } from '../../../utils/auth';
|
||||||
|
import prisma from "../../../utils/prisma";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const userId = await getUserIdFromSession(event);
|
||||||
|
|
||||||
|
// Simple admin check
|
||||||
|
if (userId !== 1) {
|
||||||
|
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
coins: {
|
||||||
|
increment: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Added 1000 coins. New balance: ${user.coins}` };
|
||||||
|
});
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// server/api/admin/village/complete-clearing.post.ts
|
|
||||||
import { getUserIdFromSession } from '../../../utils/auth';
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { REWARDS } from '../../../utils/economy';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
|
||||||
const userId = await getUserIdFromSession(event);
|
|
||||||
|
|
||||||
// Simple admin check
|
|
||||||
if (userId !== 1) {
|
|
||||||
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const village = await prisma.village.findUniqueOrThrow({ where: { userId } });
|
|
||||||
|
|
||||||
const tilesToComplete = await prisma.villageTile.findMany({
|
|
||||||
where: {
|
|
||||||
villageId: village.id,
|
|
||||||
terrainState: 'CLEARING',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tilesToComplete.length === 0) {
|
|
||||||
return { success: true, message: 'No clearing tasks to complete.' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalCoins = tilesToComplete.length * REWARDS.VILLAGE.CLEARING.coins;
|
|
||||||
const totalExp = tilesToComplete.length * REWARDS.VILLAGE.CLEARING.exp;
|
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
|
||||||
// 1. Update user totals
|
|
||||||
await tx.user.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: {
|
|
||||||
coins: { increment: totalCoins },
|
|
||||||
exp: { increment: totalExp },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Update all the tiles
|
|
||||||
await tx.villageTile.updateMany({
|
|
||||||
where: { id: { in: tilesToComplete.map(t => t.id) } },
|
|
||||||
data: { terrainState: 'IDLE', terrainType: 'EMPTY', clearingStartedAt: null },
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Create an event for each completed tile
|
|
||||||
for (const tile of tilesToComplete) {
|
|
||||||
await tx.villageEvent.create({
|
|
||||||
data: {
|
|
||||||
villageId: village.id,
|
|
||||||
type: tile.terrainType === 'BLOCKED_TREE' ? 'CLEAR_TREE' : 'CLEAR_STONE',
|
|
||||||
message: `Finished clearing ${tile.terrainType === 'BLOCKED_TREE' ? 'a tree' : 'a stone'} at (${tile.x}, ${tile.y})`,
|
|
||||||
tileX: tile.x,
|
|
||||||
tileY: tile.y,
|
|
||||||
coins: REWARDS.VILLAGE.CLEARING.coins,
|
|
||||||
exp: REWARDS.VILLAGE.CLEARING.exp,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true, message: `Completed ${tilesToComplete.length} clearing tasks.` };
|
|
||||||
});
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const yesterday = new Date(now - 24 * 60 * 60 * 1000);
|
const yesterday = new Date(now - 24 * 60 * 60 * 1000);
|
||||||
const clearingFastForwardDate = new Date(now - CLEANING_TIME_MS + 5000); // 5 seconds past completion
|
const clearingFastForwardDate = new Date(now - CLEANING_TIME_MS - 1000); // 1 second safely in the past
|
||||||
|
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
// 1. Fast-forward any tiles that are currently being cleared
|
// 1. Fast-forward any tiles that are currently being cleared
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user