Made changes to CSS and merged together.
This commit is contained in:
36
web/vue-app/src/assets/actions-shared.css
Normal file
36
web/vue-app/src/assets/actions-shared.css
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 3rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.actions button {
|
||||||
|
padding: 1rem 2.2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unified error style */
|
||||||
|
.error {
|
||||||
|
color: #e53e3e;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(255, 107, 107, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.actions {
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
.actions button {
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
54
web/vue-app/src/assets/button-shared.css
Normal file
54
web/vue-app/src/assets/button-shared.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/* Base button style */
|
||||||
|
.btn {
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.7rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.08);
|
||||||
|
transition:
|
||||||
|
background 0.18s,
|
||||||
|
color 0.18s;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary button (e.g., Save, Confirm) */
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus {
|
||||||
|
background: #5a67d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary button (e.g., Cancel) */
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f3f3f3;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover,
|
||||||
|
.btn-secondary:focus {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger button (e.g., Delete) */
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-danger:hover,
|
||||||
|
.btn-danger:focus {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green button (e.g., Confirm) */
|
||||||
|
.btn-green {
|
||||||
|
background: #22c55e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-green:hover,
|
||||||
|
.btn-green:focus {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
75
web/vue-app/src/assets/edit-forms.css
Normal file
75
web/vue-app/src/assets/edit-forms.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
.edit-view,
|
||||||
|
.child-edit-view,
|
||||||
|
.reward-edit-view,
|
||||||
|
.task-edit-view {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 24px #667eea22;
|
||||||
|
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-view h2,
|
||||||
|
.child-edit-view h2,
|
||||||
|
.reward-edit-view h2,
|
||||||
|
.task-edit-view h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group,
|
||||||
|
.reward-form label,
|
||||||
|
.task-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #444;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='number'],
|
||||||
|
.reward-form input[type='text'],
|
||||||
|
.reward-form input[type='number'],
|
||||||
|
.task-form input[type='text'],
|
||||||
|
.task-form input[type='number'] {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.cancel,
|
||||||
|
button[type='button'] {
|
||||||
|
background: #f3f3f3;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.btn.save,
|
||||||
|
button[type='submit'] {
|
||||||
|
background: #667eea;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.18s;
|
||||||
|
}
|
||||||
|
.btn.save:hover,
|
||||||
|
button[type='submit']:hover:not(:disabled) {
|
||||||
|
background: #5a67d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
108
web/vue-app/src/assets/layout-shared.css
Normal file
108
web/vue-app/src/assets/layout-shared.css
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/* Root layout */
|
||||||
|
.layout-root {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--header-bg, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top bar */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 5px 5px;
|
||||||
|
height: 48px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back button container and login button container */
|
||||||
|
.back-btn-container,
|
||||||
|
.login-btn-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn-container {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn-container {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacer for layouts without a center nav */
|
||||||
|
.spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back button */
|
||||||
|
.back-btn {
|
||||||
|
background: var(--button-bg, #fff);
|
||||||
|
border: 0;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--button-text, #667eea);
|
||||||
|
font-weight: 600;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login button inside login-btn container */
|
||||||
|
.login-btn {
|
||||||
|
background: var(--button-bg, #fff);
|
||||||
|
border: 0;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--button-text, #667eea);
|
||||||
|
font-weight: 600;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content area */
|
||||||
|
.main-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App version display */
|
||||||
|
.app-version {
|
||||||
|
position: fixed;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 12px;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--app-version, #cbd5e1);
|
||||||
|
opacity: 0.85;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.back-btn,
|
||||||
|
.login-btn button {
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
web/vue-app/src/assets/list-shared.css
Normal file
120
web/vue-app/src/assets/list-shared.css
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/* List container */
|
||||||
|
.listbox {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(100vh - 4.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: 0.2rem 0 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.7rem;
|
||||||
|
background: #fff5;
|
||||||
|
padding: 0.2rem 0.2rem 0.2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List item */
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 2px outset #38c172;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.2rem 1rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: border 0.18s;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.list-item.bad {
|
||||||
|
border-color: #e53e3e;
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
.list-item.good {
|
||||||
|
border-color: #38c172;
|
||||||
|
background: #f0fff4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image styles */
|
||||||
|
.list-image {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-right: 0.7rem;
|
||||||
|
background: #eee;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Name/label styles */
|
||||||
|
.list-name {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Points/cost/requested text */
|
||||||
|
.list-value {
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete button */
|
||||||
|
.delete-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0.15rem;
|
||||||
|
margin-left: 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition:
|
||||||
|
background 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #ffeaea;
|
||||||
|
box-shadow: 0 0 0 2px #ef444422;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.delete-btn svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.list-checkbox {
|
||||||
|
margin-left: 1rem;
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
accent-color: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading, error, empty states */
|
||||||
|
.loading,
|
||||||
|
.empty {
|
||||||
|
margin: 1.2rem 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separator (if needed) */
|
||||||
|
.list-separator {
|
||||||
|
height: 0px;
|
||||||
|
background: #0000;
|
||||||
|
margin: 0rem 0.2rem;
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
112
web/vue-app/src/assets/view-shared.css
Normal file
112
web/vue-app/src/assets/view-shared.css
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: white;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Backdrop and Modal */
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
min-width: 240px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dialog Message */
|
||||||
|
.dialog-message {
|
||||||
|
font-size: 1.08rem;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dialog-message .child-name {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Sections (Reward/Task) */
|
||||||
|
.info,
|
||||||
|
.reward-info,
|
||||||
|
.task-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.image,
|
||||||
|
.reward-image,
|
||||||
|
.task-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
.details,
|
||||||
|
.reward-details,
|
||||||
|
.task-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.name,
|
||||||
|
.reward-name,
|
||||||
|
.task-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.points,
|
||||||
|
.reward-points,
|
||||||
|
.task-points {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.main {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
padding: 1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import AssignTasksDialog from './AssignTasksDialog.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{ childId: string | number | null }>()
|
|
||||||
const showDialog = ref(false)
|
|
||||||
const openDialog = () => {
|
|
||||||
showDialog.value = true
|
|
||||||
}
|
|
||||||
const closeDialog = () => {
|
|
||||||
showDialog.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<button class="assign-tasks-btn" @click="openDialog">Assign Task</button>
|
|
||||||
<AssignTasksDialog v-if="showDialog" :child-id="props.childId" @close="closeDialog" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.assign-tasks-btn {
|
|
||||||
background: #667eea;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.7rem 1.4rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 2rem;
|
|
||||||
transition: background 0.18s;
|
|
||||||
}
|
|
||||||
.assign-tasks-btn:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -355,7 +355,7 @@ onBeforeUnmount(() => {
|
|||||||
<p>Are you sure you want to permanently delete this child?</p>
|
<p>Are you sure you want to permanently delete this child?</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
class="btn cancel"
|
class="btn btn-secondary"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
confirmDeleteVisible = false
|
confirmDeleteVisible = false
|
||||||
@@ -365,8 +365,8 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button class="btn delete" @click="performDelete" :disabled="deleting">
|
<button class="btn btn-danger" @click="performDelete" :disabled="deleting">
|
||||||
{{ deleting ? 'Deleting…' : 'Confirm Delete' }}
|
{{ deleting ? 'Deleting…' : 'Delete' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,7 +402,6 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading,
|
.loading,
|
||||||
.error,
|
|
||||||
.empty {
|
.empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -413,13 +412,6 @@ h1 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #ff6b6b;
|
|
||||||
background: rgba(255, 107, 107, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
@@ -462,6 +454,7 @@ h1 {
|
|||||||
color: #666;
|
color: #666;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* consistent focus ring without changing layout */
|
/* consistent focus ring without changing layout */
|
||||||
@@ -473,27 +466,46 @@ h1 {
|
|||||||
/* Menu overlays the card and does NOT alter flow */
|
/* Menu overlays the card and does NOT alter flow */
|
||||||
.kebab-menu {
|
.kebab-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 44px; /* place below the button */
|
top: 44px;
|
||||||
right: 0; /* align to kebab button's right edge */
|
right: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
background: white;
|
background: #f7fafcc7; /* subtle light gray for contrast */
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
border: 1.5px solid #bcc1c9; /* subtle border for definition */
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(0, 0, 0, 0.18),
|
||||||
|
0 1.5px 6px rgba(102, 126, 234, 0.08); /* stronger shadow for separation */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
|
backdrop-filter: blur(11px); /* optional: adds a modern blur effect */
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
padding: 0.6rem 0.9rem;
|
padding: 1.1rem 0.9rem; /* Increase vertical padding for bigger touch area */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
font-size: 1.1rem; /* Slightly larger text for readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item + .menu-item {
|
||||||
|
margin-top: 0.5rem; /* Add space between menu items */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.menu-item {
|
||||||
|
padding: 0.85rem 0.7rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.menu-item + .menu-item {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
.menu-item:hover {
|
||||||
@@ -564,31 +576,6 @@ h1 {
|
|||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 0.8rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.cancel {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.delete {
|
|
||||||
background: #ff4d4f;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.points {
|
.points {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
color: #444;
|
color: #444;
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ async function resizeImageFile(
|
|||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".png,.jpg,.jpeg,.gif,image/png,image/jpeg,image/gif"
|
accept=".png,.jpg,.jpeg,.gif,image/png,image/jpeg,image/gif"
|
||||||
|
capture="environment"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
@change="onFileChange"
|
@change="onFileChange"
|
||||||
/>
|
/>
|
||||||
@@ -253,15 +254,15 @@ async function resizeImageFile(
|
|||||||
<div v-if="!capturedImageUrl">
|
<div v-if="!capturedImageUrl">
|
||||||
<video ref="cameraVideo" autoplay playsinline class="camera-video"></video>
|
<video ref="cameraVideo" autoplay playsinline class="camera-video"></video>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn save" @click="takePhoto">Take Photo</button>
|
<button type="button" class="btn btn-primary" @click="takePhoto">Take Photo</button>
|
||||||
<button type="button" class="btn cancel" @click="closeCamera">Cancel</button>
|
<button type="button" class="btn btn-secondary" @click="closeCamera">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<img :src="capturedImageUrl" class="captured-preview" alt="Preview" />
|
<img :src="capturedImageUrl" class="captured-preview" alt="Preview" />
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn save" @click="confirmPhoto">Use Photo</button>
|
<button type="button" class="btn btn-primary" @click="confirmPhoto">Use Photo</button>
|
||||||
<button type="button" class="btn cancel" @click="retakePhoto">Retake</button>
|
<button type="button" class="btn btn-secondary" @click="retakePhoto">Retake</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,8 +286,8 @@ async function resizeImageFile(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.selectable-image {
|
.selectable-image {
|
||||||
width: 54px;
|
width: 64px;
|
||||||
height: 54px;
|
height: 64px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 2px solid #e6e6e6;
|
border: 2px solid #e6e6e6;
|
||||||
@@ -310,29 +311,31 @@ async function resizeImageFile(
|
|||||||
|
|
||||||
.image-actions {
|
.image-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 4rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 0.5rem;
|
margin-top: 1.2rem; /* Increased space below images */
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 38px;
|
width: 56px; /* Increased size */
|
||||||
height: 38px;
|
height: 56px; /* Increased size */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.18s;
|
transition: background 0.18s;
|
||||||
font-size: 1.5rem;
|
font-size: 2.2rem; /* Bigger + icon */
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.07);
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.07);
|
||||||
}
|
}
|
||||||
.icon-btn:hover {
|
.icon-btn svg {
|
||||||
background: #e0e7ff;
|
width: 32px; /* Bigger camera icon */
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -56,8 +56,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-button-root">
|
<div class="login-button-root">
|
||||||
<button v-if="!isParentAuthenticated" @click="open" aria-label="Parent login">Parent</button>
|
<button v-if="!isParentAuthenticated" @click="open" aria-label="Parent login" class="login-btn">
|
||||||
<button v-else @click="handleLogout" aria-label="Parent logout">Log out</button>
|
Parent
|
||||||
|
</button>
|
||||||
|
<button v-else @click="handleLogout" aria-label="Parent logout" class="login-btn">
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
|
||||||
<div v-if="show" class="modal-backdrop" @click.self="close">
|
<div v-if="show" class="modal-backdrop" @click.self="close">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
@@ -73,8 +77,8 @@ onUnmounted(() => {
|
|||||||
class="pin-input"
|
class="pin-input"
|
||||||
/>
|
/>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn cancel" @click="close">Cancel</button>
|
<button type="button" class="btn btn-secondary" @click="close">Cancel</button>
|
||||||
<button type="submit" class="btn submit">OK</button>
|
<button type="submit" class="btn btn-primary">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
@@ -132,30 +136,6 @@ onUnmounted(() => {
|
|||||||
margin-bottom: 0.4rem;
|
margin-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.45rem 0.7rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.cancel {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.submit {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #ff6b6b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button-root {
|
.login-button-root {
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.points .value {
|
.points .value {
|
||||||
font-size: 1.1rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 700;
|
font-weight: 900;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn cancel" @click="onCancel" :disabled="loading">
|
<button type="button" class="btn btn-secondary" @click="onCancel" :disabled="loading">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" class="btn save" :disabled="loading">
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
{{ isEdit ? 'Save' : 'Create' }}
|
{{ isEdit ? 'Save' : 'Create' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import ImagePicker from '../ImagePicker.vue'
|
import ImagePicker from '../ImagePicker.vue'
|
||||||
|
import '@/assets/edit-forms.css'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -153,22 +154,8 @@ function onCancel() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Removed duplicated CSS now present in edit-forms.css -->
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.child-edit-view {
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 2rem auto;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 24px #667eea22;
|
|
||||||
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -204,44 +191,8 @@ input:focus {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border: 1.5px solid #667eea;
|
border: 1.5px solid #667eea;
|
||||||
}
|
}
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.7rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1.1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.18s;
|
|
||||||
}
|
|
||||||
.btn.cancel {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.btn.save {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn.save:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
.form-group.image-picker-group {
|
.form-group.image-picker-group {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.error {
|
|
||||||
color: #e53e3e;
|
|
||||||
margin-top: 0.7rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.loading-message {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { defineProps, defineEmits, ref, watch } from 'vue'
|
|
||||||
import ImagePicker from '../ImagePicker.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
child: { id: string | number; name: string; age: number; image_id?: string | null } | null
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits(['close', 'updated'])
|
|
||||||
|
|
||||||
const name = ref(props.child?.name ?? '')
|
|
||||||
const age = ref(props.child?.age ?? '')
|
|
||||||
const selectedImageId = ref<string | null>(props.child?.image_id ?? null)
|
|
||||||
const localImageFile = ref<File | null>(null)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.child,
|
|
||||||
(c) => {
|
|
||||||
name.value = c?.name ?? ''
|
|
||||||
age.value = c?.age ?? ''
|
|
||||||
selectedImageId.value = c?.image_id ?? null
|
|
||||||
localImageFile.value = null
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle new image from ImagePicker
|
|
||||||
function onAddImage({ id, file }: { id: string; file: File }) {
|
|
||||||
if (id === 'local-upload') {
|
|
||||||
localImageFile.value = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
let imageId = selectedImageId.value
|
|
||||||
|
|
||||||
// If the selected image is a local upload, upload it first
|
|
||||||
if (imageId === 'local-upload' && localImageFile.value) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', localImageFile.value)
|
|
||||||
formData.append('type', '1')
|
|
||||||
formData.append('permanent', 'false')
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/image/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
if (!resp.ok) throw new Error('Image upload failed')
|
|
||||||
const data = await resp.json()
|
|
||||||
imageId = data.id
|
|
||||||
} catch (err) {
|
|
||||||
alert('Failed to upload image.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now update the child
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/api/child/${props.child?.id}/edit`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: name.value,
|
|
||||||
age: age.value,
|
|
||||||
image_id: imageId,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!resp.ok) throw new Error('Failed to update child')
|
|
||||||
emit('updated')
|
|
||||||
} catch (err) {
|
|
||||||
alert('Failed to update child.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="modal-backdrop">
|
|
||||||
<div class="modal">
|
|
||||||
<h3>Edit Child</h3>
|
|
||||||
<form @submit.prevent="submit" class="form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="child-name">Name</label>
|
|
||||||
<input id="child-name" v-model="name" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="child-age">Age</label>
|
|
||||||
<input id="child-age" v-model="age" type="number" min="0" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group image-picker-group">
|
|
||||||
<label for="child-image">Image</label>
|
|
||||||
<ImagePicker
|
|
||||||
id="child-image"
|
|
||||||
v-model="selectedImageId"
|
|
||||||
:image-type="1"
|
|
||||||
@add-image="onAddImage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button type="button" class="btn cancel" @click="emit('close')">Cancel</button>
|
|
||||||
<button type="submit" class="btn save">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1200;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: max(3vh, env(safe-area-inset-top, 24px));
|
|
||||||
padding-bottom: max(3vh, env(safe-area-inset-bottom, 24px));
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
color: #222;
|
|
||||||
padding: 1.2rem 2rem 1.2rem 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 360px;
|
|
||||||
max-width: 100vw;
|
|
||||||
max-height: 100vh;
|
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
|
||||||
text-align: center;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px), (max-height: 480px) {
|
|
||||||
.modal {
|
|
||||||
width: 90vw;
|
|
||||||
max-width: 98vw;
|
|
||||||
max-height: 94vh;
|
|
||||||
padding: 0.7rem 0.7rem 0.7rem 0.7rem;
|
|
||||||
font-size: 0.97rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (orientation: landscape) and (min-width: 601px) {
|
|
||||||
.modal {
|
|
||||||
width: 75vw;
|
|
||||||
max-width: 600px;
|
|
||||||
max-height: 94vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.35rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='text'],
|
|
||||||
input[type='number'] {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0.5rem 0.7rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e6e6e6;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #fafbff;
|
|
||||||
color: #222;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
border: 1.5px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.7rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1.1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.18s;
|
|
||||||
}
|
|
||||||
.btn.cancel {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.btn.save {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.btn.save:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group.image-picker-group {
|
|
||||||
display: block;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -5,6 +5,7 @@ import ChildDetailCard from './ChildDetailCard.vue'
|
|||||||
import ChildTaskList from '../task/ChildTaskList.vue'
|
import ChildTaskList from '../task/ChildTaskList.vue'
|
||||||
import ChildRewardList from '../reward/ChildRewardList.vue'
|
import ChildRewardList from '../reward/ChildRewardList.vue'
|
||||||
import { eventBus } from '@/common/eventBus'
|
import { eventBus } from '@/common/eventBus'
|
||||||
|
import '@/assets/view-shared.css'
|
||||||
import type {
|
import type {
|
||||||
Child,
|
Child,
|
||||||
Event,
|
Event,
|
||||||
@@ -252,7 +253,7 @@ function resetInactivityTimer() {
|
|||||||
if (inactivityTimer) clearTimeout(inactivityTimer)
|
if (inactivityTimer) clearTimeout(inactivityTimer)
|
||||||
inactivityTimer = setTimeout(() => {
|
inactivityTimer = setTimeout(() => {
|
||||||
router.push({ name: 'ChildrenListView' })
|
router.push({ name: 'ChildrenListView' })
|
||||||
}, 15000) // 15 seconds
|
}, 60000) // 60 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupInactivityListeners() {
|
function setupInactivityListeners() {
|
||||||
@@ -310,7 +311,7 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
<div v-else-if="error" class="error">Error: {{ error }}</div>
|
<div v-else-if="error" class="error">Error: {{ error }}</div>
|
||||||
|
|
||||||
@@ -363,8 +364,8 @@ onUnmounted(() => {
|
|||||||
Would you like to redeem this reward?
|
Would you like to redeem this reward?
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="confirmRedeemReward">Yes</button>
|
<button @click="confirmRedeemReward" class="btn btn-primary">Yes</button>
|
||||||
<button @click="cancelRedeemReward">No</button>
|
<button @click="cancelRedeemReward" class="btn btn-secondary">No</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,8 +389,8 @@ onUnmounted(() => {
|
|||||||
Would you like to cancel the pending reward request?
|
Would you like to cancel the pending reward request?
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="cancelPendingReward">Yes</button>
|
<button @click="cancelPendingReward" class="btn btn-primary">Yes</button>
|
||||||
<button @click="closeCancelDialog">No</button>
|
<button @click="closeCancelDialog" class="btn btn-secondary">No</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,15 +398,6 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
background: white;
|
background: white;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -416,153 +408,42 @@ onUnmounted(() => {
|
|||||||
color: #667eea;
|
color: #667eea;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.loading,
|
|
||||||
.error {
|
.assign-buttons {
|
||||||
color: white;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #ff6b6b;
|
|
||||||
background: rgba(255, 107, 107, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
/* Remove grid styles */
|
|
||||||
/* grid-template-columns: 1fr 320px; */
|
|
||||||
/* gap: 1.5rem; */
|
|
||||||
}
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.side {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 120px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
.modal-backdrop {
|
.assign-task-btn,
|
||||||
position: fixed;
|
.assign-bad-btn,
|
||||||
top: 0;
|
.assign-reward-btn {
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.35);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 24px #667eea22;
|
|
||||||
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
.reward-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.reward-image {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
.reward-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.reward-name {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.1rem;
|
border: none;
|
||||||
}
|
|
||||||
.reward-points {
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.dialog-message {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.7rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.actions button {
|
|
||||||
padding: 0.5rem 1.1rem;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 0;
|
padding: 0.7rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 700;
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.08);
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.18s;
|
transition: background 0.18s;
|
||||||
}
|
color: #fff;
|
||||||
.actions button:first-child {
|
|
||||||
background: #667eea;
|
background: #667eea;
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
.actions button:last-child {
|
.assign-task-btn:hover,
|
||||||
background: #f3f3f3;
|
.assign-bad-btn:hover,
|
||||||
color: #666;
|
.assign-reward-btn:hover {
|
||||||
|
background: #5a67d8;
|
||||||
}
|
}
|
||||||
.actions button:last-child:hover {
|
.assign-bad-btn {
|
||||||
background: #e2e8f0;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
.assign-bad-btn:hover {
|
||||||
/* Mobile adjustments */
|
background: #dc2626;
|
||||||
@media (max-width: 900px) {
|
|
||||||
.layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
|
.assign-reward-btn {
|
||||||
|
background: #38c172;
|
||||||
}
|
}
|
||||||
|
.assign-reward-btn:hover {
|
||||||
@media (max-width: 480px) {
|
background: #2f855a;
|
||||||
.container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.back-btn {
|
|
||||||
padding: 0.45rem 0.75rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.main {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
padding: 0.75rem;
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ChildDetailCard from './ChildDetailCard.vue'
|
|||||||
import ChildTaskList from '../task/ChildTaskList.vue'
|
import ChildTaskList from '../task/ChildTaskList.vue'
|
||||||
import ChildRewardList from '../reward/ChildRewardList.vue'
|
import ChildRewardList from '../reward/ChildRewardList.vue'
|
||||||
import { eventBus } from '@/common/eventBus'
|
import { eventBus } from '@/common/eventBus'
|
||||||
|
import '@/assets/view-shared.css'
|
||||||
import type {
|
import type {
|
||||||
Task,
|
Task,
|
||||||
Child,
|
Child,
|
||||||
@@ -332,7 +333,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
<div v-else-if="error" class="error">Error: {{ error }}</div>
|
<div v-else-if="error" class="error">Error: {{ error }}</div>
|
||||||
|
|
||||||
@@ -367,13 +368,11 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="assign-buttons">
|
<div class="assign-buttons">
|
||||||
<button v-if="child" class="assign-task-btn" @click="goToAssignTasks">Assign Tasks</button>
|
<button v-if="child" class="btn btn-primary" @click="goToAssignTasks">Assign Tasks</button>
|
||||||
<button v-if="child" class="assign-bad-btn" @click="goToAssignBadHabits">
|
<button v-if="child" class="btn btn-danger" @click="goToAssignBadHabits">
|
||||||
Assign Bad Habits
|
Assign Bad Habits
|
||||||
</button>
|
</button>
|
||||||
<button v-if="child" class="assign-reward-btn" @click="goToAssignRewards">
|
<button v-if="child" class="btn btn-green" @click="goToAssignRewards">Assign Rewards</button>
|
||||||
Assign Rewards
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pending Reward Dialog -->
|
<!-- Pending Reward Dialog -->
|
||||||
@@ -385,8 +384,8 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
Would you like to cancel the pending reward?
|
Would you like to cancel the pending reward?
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="cancelPendingReward">Yes, Cancel Reward</button>
|
<button @click="cancelPendingReward" class="btn btn-primary">Yes</button>
|
||||||
<button @click="showPendingRewardDialog = false">No</button>
|
<button @click="showPendingRewardDialog = false" class="btn btn-secondary">No</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,7 +402,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
<div class="task-details">
|
<div class="task-details">
|
||||||
<div class="task-name">{{ selectedTask.name }}</div>
|
<div class="task-name">{{ selectedTask.name }}</div>
|
||||||
<div class="task-points" :class="selectedTask.is_good ? 'good' : 'bad'">
|
<div class="task-points" :class="selectedTask.is_good ? 'good' : 'bad'">
|
||||||
{{ selectedTask.points }} pts
|
{{ selectedTask.points }} points
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,7 +412,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
<span class="child-name">{{ child?.name }}</span>
|
<span class="child-name">{{ child?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="confirmTriggerTask">Yes</button>
|
<button @click="confirmTriggerTask" class="btn btn-primary">Yes</button>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
@@ -421,6 +420,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
selectedTask = null
|
selectedTask = null
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
class="btn btn-secondary"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -443,7 +443,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
{{
|
{{
|
||||||
selectedReward.points_needed === 0
|
selectedReward.points_needed === 0
|
||||||
? 'Reward Ready!'
|
? 'Reward Ready!'
|
||||||
: selectedReward.points_needed + ' pts needed'
|
: selectedReward.points_needed + ' more points'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,7 +453,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
>?
|
>?
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="confirmTriggerReward">Yes</button>
|
<button @click="confirmTriggerReward" class="btn btn-primary">Yes</button>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
@@ -461,6 +461,7 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
selectedReward = null
|
selectedReward = null
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
class="btn btn-secondary"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -471,174 +472,6 @@ const childId = computed(() => child.value?.id ?? null)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 2rem;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.loading,
|
|
||||||
.error {
|
|
||||||
color: white;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #ff6b6b;
|
|
||||||
background: rgba(255, 107, 107, 0.1);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal styles */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.45);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1200;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
background: #fff;
|
|
||||||
color: #222;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
min-width: 240px;
|
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.task-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.task-image {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
.task-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.task-points,
|
|
||||||
.task-points.good,
|
|
||||||
.task-points.bad {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.task-points.good {
|
|
||||||
color: #38c172;
|
|
||||||
}
|
|
||||||
.task-points.bad {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
margin-top: 1.2rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.actions button {
|
|
||||||
padding: 0.5rem 1.2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.actions button:first-child {
|
|
||||||
background: #667eea;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.actions button:last-child {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.actions button:last-child:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile adjustments */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.layout {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.main {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-message {
|
|
||||||
font-size: 1.08rem;
|
|
||||||
color: #444;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.dialog-message .child-name {
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
.reward-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.reward-image {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
.reward-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.reward-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.reward-points {
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.assign-buttons {
|
.assign-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-if="rewardCountRef != 0">
|
<div class="actions" v-if="rewardCountRef != 0">
|
||||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
<button class="btn btn-secondary" @click="onCancel">Cancel</button>
|
||||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
<button class="btn btn-primary" @click="onSubmit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -80,24 +80,30 @@ h2 {
|
|||||||
margin: 0.2rem;
|
margin: 0.2rem;
|
||||||
}
|
}
|
||||||
.reward-list-scroll {
|
.reward-list-scroll {
|
||||||
flex: 1 1 auto;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
justify-content: center;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 3rem; /* Increased gap for more space between buttons */
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
margin-top: 0;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.btn {
|
.actions button {
|
||||||
padding: 0.5rem 1.2rem;
|
padding: 1rem 2.2rem; /* Bigger touch area */
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
border: none;
|
border: 0;
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem; /* Larger text */
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 120px; /* Ensures buttons are wide enough */
|
||||||
}
|
}
|
||||||
.btn.cancel {
|
.btn.cancel {
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" v-if="taskCountRef > 0">
|
<div class="actions" v-if="taskCountRef > 0">
|
||||||
<button class="btn cancel" @click="onCancel">Cancel</button>
|
<button class="btn btn-secondary" @click="onCancel">Cancel</button>
|
||||||
<button class="btn submit" @click="onSubmit">Submit</button>
|
<button class="btn btn-primary" @click="onSubmit">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -87,24 +87,30 @@ h2 {
|
|||||||
margin: 0.2rem;
|
margin: 0.2rem;
|
||||||
}
|
}
|
||||||
.task-list-scroll {
|
.task-list-scroll {
|
||||||
flex: 1 1 auto;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
justify-content: center;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 3rem; /* Increased gap for more space between buttons */
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
margin-top: 0;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.btn {
|
.actions button {
|
||||||
padding: 0.5rem 1.2rem;
|
padding: 1rem 2.2rem; /* Bigger touch area */
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
border: none;
|
border: 0;
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem; /* Larger text */
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 120px; /* Ensures buttons are wide enough */
|
||||||
}
|
}
|
||||||
.btn.cancel {
|
.btn.cancel {
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
|||||||
import { getCachedImageUrl } from '../../common/imageCache'
|
import { getCachedImageUrl } from '../../common/imageCache'
|
||||||
import type { PendingReward, Event, ChildRewardRequestEventPayload } from '@/common/models'
|
import type { PendingReward, Event, ChildRewardRequestEventPayload } from '@/common/models'
|
||||||
import { eventBus } from '@/common/eventBus'
|
import { eventBus } from '@/common/eventBus'
|
||||||
|
import '@/assets/list-shared.css'
|
||||||
|
|
||||||
const emit = defineEmits(['item-clicked'])
|
const emit = defineEmits(['item-clicked'])
|
||||||
|
|
||||||
@@ -70,19 +71,19 @@ function handleItemClick(item: PendingReward) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="centered-list-container">
|
||||||
<div v-if="loading" class="loading">Loading notifications...</div>
|
<div v-if="loading" class="loading">Loading notifications...</div>
|
||||||
<div v-else-if="error" class="error">{{ error }}</div>
|
<div v-else-if="error" class="error">{{ error }}</div>
|
||||||
<div v-else-if="notifications.length === 0" class="empty">No Notifications</div>
|
<div v-else-if="notifications.length === 0" class="empty">No Notifications</div>
|
||||||
<div v-else class="notification-listbox">
|
<div v-else class="listbox">
|
||||||
<div v-for="(item, idx) in notifications" :key="item.id">
|
<div v-for="(item, idx) in notifications" :key="item.id">
|
||||||
<div class="notification-list-item" @click="handleItemClick(item)">
|
<div class="list-item notification-centered" @click="handleItemClick(item)">
|
||||||
<div class="child-info">
|
<div class="child-info">
|
||||||
<img
|
<img
|
||||||
v-if="item.child_image_url"
|
v-if="item.child_image_url"
|
||||||
:src="item.child_image_url"
|
:src="item.child_image_url"
|
||||||
alt="Child"
|
alt="Child"
|
||||||
class="child-image"
|
class="list-image"
|
||||||
/>
|
/>
|
||||||
<span class="child-name">{{ item.child_name }}</span>
|
<span class="child-name">{{ item.child_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,61 +94,30 @@ function handleItemClick(item: PendingReward) {
|
|||||||
v-if="item.reward_image_url"
|
v-if="item.reward_image_url"
|
||||||
:src="item.reward_image_url"
|
:src="item.reward_image_url"
|
||||||
alt="Reward"
|
alt="Reward"
|
||||||
class="reward-image"
|
class="list-image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="idx < notifications.length - 1" class="notification-separator"></div>
|
<div v-if="idx < notifications.length - 1" class="list-separator"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notification-listbox {
|
.centered-list-container {
|
||||||
flex: 1 1 auto;
|
width: 100%;
|
||||||
width: auto;
|
|
||||||
max-width: 480px;
|
|
||||||
max-height: calc(100vh - 4.5rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0.2rem 0 0 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
gap: 0.7rem;
|
|
||||||
background: #fff5;
|
|
||||||
padding: 0.2rem 0.2rem 0.2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
.notification-list-item {
|
.notification-centered {
|
||||||
display: flex;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
border: 2px outset #ef4444;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.2rem 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: border 0.18s;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
|
|
||||||
box-sizing: border-box;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
.child-info {
|
.child-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
}
|
}
|
||||||
.child-image {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #eee;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.child-name {
|
.child-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
@@ -161,37 +131,6 @@ function handleItemClick(item: PendingReward) {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
.reward-image {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #eee;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.loading,
|
|
||||||
.error,
|
|
||||||
.empty {
|
|
||||||
margin: 2rem 0;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
color: #fdfdfd;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #ef4444; /* Red-500 for errors */
|
|
||||||
background: #fff1f2; /* Red-50 for error background */
|
|
||||||
}
|
|
||||||
.notification-list-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.notification-separator {
|
|
||||||
height: 0px;
|
|
||||||
background: #0000;
|
|
||||||
margin: 0rem 0.2rem;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
.requested-text {
|
.requested-text {
|
||||||
margin: 0 0.7rem;
|
margin: 0 0.7rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const isAnyPending = computed(() => rewards.value.some((r) => r.redeeming))
|
|||||||
<img v-if="r.image_id" :src="r.image_id" alt="Reward Image" class="reward-image" />
|
<img v-if="r.image_id" :src="r.image_id" alt="Reward Image" class="reward-image" />
|
||||||
<div class="reward-points" :class="{ ready: r.points_needed === 0 }">
|
<div class="reward-points" :class="{ ready: r.points_needed === 0 }">
|
||||||
<template v-if="r.points_needed === 0"> REWARD READY </template>
|
<template v-if="r.points_needed === 0"> REWARD READY </template>
|
||||||
<template v-else> {{ r.points_needed }} pts needed </template>
|
<template v-else> {{ r.points_needed }} more points </template>
|
||||||
</div>
|
</div>
|
||||||
<!-- PENDING block if redeeming is true -->
|
<!-- PENDING block if redeeming is true -->
|
||||||
<div v-if="r.redeeming" class="pending-block">PENDING</div>
|
<div v-if="r.redeeming" class="pending-block">PENDING</div>
|
||||||
@@ -316,6 +316,13 @@ const isAnyPending = computed(() => rewards.value.some((r) => r.redeeming))
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #ffd166;
|
color: #ffd166;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 0 #1a3d1f,
|
||||||
|
1px -1px 0 #1a3d1f,
|
||||||
|
-1px 1px 0 #1a3d1f,
|
||||||
|
1px 1px 0 #1a3d1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reward-points.ready {
|
.reward-points.ready {
|
||||||
|
|||||||
@@ -26,8 +26,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" @click="handleCancel" :disabled="loading">Cancel</button>
|
<button type="button" @click="handleCancel" :disabled="loading" class="btn btn-secondary">
|
||||||
<button type="submit" :disabled="loading">
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="loading" class="btn btn-primary">
|
||||||
{{ isEdit ? 'Save' : 'Create' }}
|
{{ isEdit ? 'Save' : 'Create' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
import { ref, onMounted, computed, nextTick } from 'vue'
|
import { ref, onMounted, computed, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import ImagePicker from '../ImagePicker.vue'
|
import ImagePicker from '../ImagePicker.vue'
|
||||||
|
import '@/assets/edit-forms.css'
|
||||||
|
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -158,87 +161,47 @@ const submit = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.reward-edit-view {
|
.good-bad-toggle {
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 24px #667eea22;
|
|
||||||
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 0.5rem;
|
||||||
overflow-y: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.reward-form label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 1.1rem;
|
margin-bottom: 1.1rem;
|
||||||
font-weight: 500;
|
justify-content: flex-start;
|
||||||
color: #444;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.reward-form input[type='text'],
|
|
||||||
.reward-form input[type='number'] {
|
button.toggle-btn {
|
||||||
display: block;
|
flex: 1 1 0;
|
||||||
width: 100%;
|
padding: 0.5rem 1.2rem;
|
||||||
margin-top: 0.4rem;
|
border-width: 2px;
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
button[type='submit'] {
|
|
||||||
background: #667eea;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1.4rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.18s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type='submit']:hover:not(:disabled) {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type='button'] {
|
|
||||||
background: #f3f3f3;
|
background: #f3f3f3;
|
||||||
color: #666;
|
color: #444;
|
||||||
border: none;
|
transition:
|
||||||
border-radius: 8px;
|
background 0.18s,
|
||||||
padding: 0.6rem 1.4rem;
|
color 0.18s,
|
||||||
font-weight: 600;
|
border-style 0.18s;
|
||||||
font-size: 1rem;
|
outline: none;
|
||||||
cursor: pointer;
|
border-style: outset; /* Default style */
|
||||||
|
border-color: #cbd5e1;
|
||||||
}
|
}
|
||||||
.form-group.image-picker-group {
|
|
||||||
width: 100%;
|
button.toggle-btn.good-active {
|
||||||
|
background: #38c172;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px #38c17233;
|
||||||
|
transform: translateY(2px) scale(0.97);
|
||||||
|
border-style: ridge;
|
||||||
|
border-color: #38c172;
|
||||||
}
|
}
|
||||||
.actions {
|
|
||||||
margin-top: 1.2rem;
|
button.toggle-btn.bad-active {
|
||||||
display: flex;
|
background: #e53e3e;
|
||||||
justify-content: flex-end;
|
color: #fff;
|
||||||
gap: 1rem;
|
box-shadow: 0 2px 8px #e53e3e33;
|
||||||
width: 100%;
|
transform: translateY(2px) scale(0.97);
|
||||||
}
|
border-style: ridge;
|
||||||
.error {
|
border-color: #e53e3e;
|
||||||
color: #e53e3e;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.loading-message {
|
|
||||||
text-align: center;
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, watch, onMounted, computed } from 'vue'
|
import { ref, watch, onMounted, computed } from 'vue'
|
||||||
import { getCachedImageUrl } from '../../common/imageCache'
|
import { getCachedImageUrl } from '../../common/imageCache'
|
||||||
import type { Reward } from '@/common/models'
|
import type { Reward } from '@/common/models'
|
||||||
|
import '@/assets/list-shared.css'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
childId?: string | number
|
childId?: string | number
|
||||||
@@ -132,27 +133,27 @@ const listHeight = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="reward-listbox" :style="{ maxHeight: `min(${listHeight}, calc(100vh - 4.5rem))` }">
|
<div class="listbox" :style="{ maxHeight: `min(${listHeight}, calc(100vh - 4.5rem))` }">
|
||||||
<div v-if="loading" class="loading">Loading rewards...</div>
|
<div v-if="loading" class="loading">Loading rewards...</div>
|
||||||
<div v-else-if="error" class="error">{{ error }}</div>
|
<div v-else-if="error" class="error">{{ error }}</div>
|
||||||
<div v-else-if="rewards.length === 0" class="empty">No rewards found.</div>
|
<div v-else-if="rewards.length === 0" class="empty">No rewards found.</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="(reward, idx) in rewards" :key="reward.id">
|
<div v-for="(reward, idx) in rewards" :key="reward.id">
|
||||||
<div class="reward-list-item" @click="handleEdit(reward.id)">
|
<div class="list-item" @click="handleEdit(reward.id)">
|
||||||
<img v-if="reward.image_url" :src="reward.image_url" alt="Reward" class="reward-image" />
|
<img v-if="reward.image_url" :src="reward.image_url" alt="Reward" class="list-image" />
|
||||||
<span class="reward-name">{{ reward.name }}</span>
|
<span class="list-name">{{ reward.name }}</span>
|
||||||
<span class="reward-cost"> {{ reward.cost }} pts </span>
|
<span class="list-cost"> {{ reward.cost }} pts </span>
|
||||||
<input
|
<input
|
||||||
v-if="props.selectable"
|
v-if="props.selectable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="reward-checkbox"
|
class="list-checkbox"
|
||||||
v-model="selectedRewards"
|
v-model="selectedRewards"
|
||||||
:value="reward.id"
|
:value="reward.id"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="props.deletable"
|
v-if="props.deletable"
|
||||||
class="delete-btn"
|
class="btn btn-danger"
|
||||||
@click.stop="handleDelete(reward.id)"
|
@click.stop="handleDelete(reward.id)"
|
||||||
aria-label="Delete reward"
|
aria-label="Delete reward"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -168,109 +169,10 @@ const listHeight = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="idx < rewards.length - 1" class="reward-separator"></div>
|
<div v-if="idx < rewards.length - 1" class="list-separator"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.reward-listbox {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
/* Subtract any header/nav height if needed, e.g. 4.5rem */
|
|
||||||
max-height: calc(100vh - 4.5rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0.2rem 0 0 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.7rem;
|
|
||||||
background: #fff5;
|
|
||||||
padding: 0.2rem 0.2rem 0.2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.reward-list-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 2px outset #38c172;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.2rem 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: border 0.18s;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.reward-image {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-right: 0.7rem;
|
|
||||||
background: #eee;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.reward-name {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.reward-cost {
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.loading,
|
|
||||||
.error,
|
|
||||||
.empty {
|
|
||||||
margin: 1.2rem 0;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #e53e3e;
|
|
||||||
}
|
|
||||||
.reward-list-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.reward-separator {
|
|
||||||
height: 0px;
|
|
||||||
background: #0000;
|
|
||||||
margin: 0rem 0.2rem;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
.delete-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 0.15rem;
|
|
||||||
margin-left: 0.7rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition:
|
|
||||||
background 0.15s,
|
|
||||||
box-shadow 0.15s;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
.delete-btn:hover {
|
|
||||||
background: #ffeaea;
|
|
||||||
box-shadow: 0 0 0 2px #ef444422;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.delete-btn svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.reward-checkbox {
|
|
||||||
margin-left: 1rem;
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
accent-color: #667eea;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
<div class="modal">
|
<div class="modal">
|
||||||
<p>Are you sure you want to delete this reward?</p>
|
<p>Are you sure you want to delete this reward?</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="deleteReward">Yes, Delete</button>
|
<button @click="deleteReward" class="btn btn-danger">Delete</button>
|
||||||
<button @click="showConfirm = false">Cancel</button>
|
<button @click="showConfirm = false" class="btn btn-secondary">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,17 +106,20 @@ const createReward = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1.2rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 3rem; /* Increased gap for more space between buttons */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.actions button {
|
.actions button {
|
||||||
padding: 0.5rem 1.2rem;
|
padding: 1rem 2.2rem; /* Bigger touch area */
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
border: none;
|
border: 0;
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem; /* Larger text */
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 120px; /* Ensures buttons are wide enough */
|
||||||
}
|
}
|
||||||
.actions button:first-child {
|
.actions button:first-child {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ onBeforeUnmount(() => {
|
|||||||
class="task-points"
|
class="task-points"
|
||||||
:class="{ 'good-points': task.is_good, 'bad-points': !task.is_good }"
|
:class="{ 'good-points': task.is_good, 'bad-points': !task.is_good }"
|
||||||
>
|
>
|
||||||
{{ task.is_good ? task.points : -task.points }} pts
|
{{ task.is_good ? task.points : -task.points }} Points
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,10 +233,6 @@ onBeforeUnmount(() => {
|
|||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fallback for browsers that don't support flex gap */
|
|
||||||
.task-card + .task-card {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
.task-card {
|
.task-card {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -301,8 +297,13 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-points {
|
.task-points {
|
||||||
font-size: 0.75rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 900;
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 0 #1a3d1f,
|
||||||
|
1px -1px 0 #1a3d1f,
|
||||||
|
-1px 1px 0 #1a3d1f,
|
||||||
|
1px 1px 0 #1a3d1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-points.good-points {
|
.task-points.good-points {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, onMounted, computed, defineEmits, nextTick } from 'vue'
|
import { ref, onMounted, computed, defineEmits, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import ImagePicker from '@/components/ImagePicker.vue'
|
import ImagePicker from '@/components/ImagePicker.vue'
|
||||||
|
import '@/assets/edit-forms.css'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -175,8 +176,10 @@ function onAddImage({ id, file }: { id: string; file: File }) {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" @click="handleCancel" :disabled="loading">Cancel</button>
|
<button type="button" @click="handleCancel" :disabled="loading" class="btn btn-secondary">
|
||||||
<button type="submit" :disabled="loading">
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" :disabled="loading" class="btn btn-primary">
|
||||||
{{ isEdit ? 'Save' : 'Create' }}
|
{{ isEdit ? 'Save' : 'Create' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,77 +188,6 @@ function onAddImage({ id, file }: { id: string; file: File }) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.task-edit-view {
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 24px #667eea22;
|
|
||||||
padding: 2rem 2.2rem 1.5rem 2.2rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.task-form label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 1.1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #444;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-form input[type='text'],
|
|
||||||
.task-form input[type='number'] {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 7px;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
font-size: 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
button[type='submit'] {
|
|
||||||
background: #667eea;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1.4rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.18s;
|
|
||||||
}
|
|
||||||
button[type='submit']:hover:not(:disabled) {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[type='button'] {
|
|
||||||
background: #f3f3f3;
|
|
||||||
color: #666;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1.4rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.good-bad-toggle {
|
.good-bad-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -299,21 +231,4 @@ button.toggle-btn.bad-active {
|
|||||||
border-style: ridge;
|
border-style: ridge;
|
||||||
border-color: #e53e3e;
|
border-color: #e53e3e;
|
||||||
}
|
}
|
||||||
.actions {
|
|
||||||
margin-top: 1.2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #e53e3e;
|
|
||||||
margin-top: 0.7rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.loading-message {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, watch, onMounted, computed } from 'vue'
|
import { ref, watch, onMounted, computed } from 'vue'
|
||||||
import { getCachedImageUrl } from '../../common/imageCache'
|
import { getCachedImageUrl } from '../../common/imageCache'
|
||||||
import type { Task } from '@/common/models'
|
import type { Task } from '@/common/models'
|
||||||
|
import '@/assets/list-shared.css'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
childId?: string | number
|
childId?: string | number
|
||||||
@@ -143,37 +144,37 @@ const filteredTasks = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="task-listbox" :style="{ maxHeight: `min(${listHeight}, calc(100vh - 4.5rem))` }">
|
<div class="listbox" :style="{ maxHeight: `min(${listHeight}, calc(100vh - 4.5rem))` }">
|
||||||
<div v-if="loading" class="loading">Loading tasks...</div>
|
<div v-if="loading" class="loading">Loading tasks...</div>
|
||||||
<div v-else-if="error" class="error">{{ error }}</div>
|
<div v-else-if="error" class="error">{{ error }}</div>
|
||||||
<div v-else-if="tasks.length === 0" class="empty">No tasks found.</div>
|
<div v-else-if="tasks.length === 0" class="empty">No tasks found.</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="(task, idx) in filteredTasks" :key="task.id">
|
<div v-for="(task, idx) in filteredTasks" :key="task.id">
|
||||||
<div
|
<div
|
||||||
class="task-list-item"
|
class="list-item"
|
||||||
:class="{ good: task.is_good, bad: !task.is_good }"
|
:class="{ good: task.is_good, bad: !task.is_good }"
|
||||||
@click="handleEdit(task.id)"
|
@click="handleEdit(task.id)"
|
||||||
>
|
>
|
||||||
<img v-if="task.image_url" :src="task.image_url" alt="Task" class="task-image" />
|
<img v-if="task.image_url" :src="task.image_url" alt="Task" class="list-image" />
|
||||||
<span class="task-name">{{ task.name }}</span>
|
<span class="list-name">{{ task.name }}</span>
|
||||||
<span class="task-points">
|
<span class="list-points">
|
||||||
{{ task.is_good ? task.points : '-' + task.points }} pts
|
{{ task.is_good ? task.points : '-' + task.points }} pts
|
||||||
</span>
|
</span>
|
||||||
<!-- Add checkbox if selectable -->
|
<!-- Add checkbox if selectable -->
|
||||||
<input
|
<input
|
||||||
v-if="props.selectable"
|
v-if="props.selectable"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="task-checkbox"
|
class="list-checkbox"
|
||||||
v-model="selectedTasks"
|
v-model="selectedTasks"
|
||||||
:value="task.id"
|
:value="task.id"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="props.deletable"
|
v-if="props.deletable"
|
||||||
class="delete-btn"
|
|
||||||
@click.stop="handleDelete(task.id)"
|
@click.stop="handleDelete(task.id)"
|
||||||
aria-label="Delete task"
|
aria-label="Delete task"
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-danger"
|
||||||
>
|
>
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||||
<circle cx="10" cy="10" r="9" fill="#fff" stroke="#ef4444" stroke-width="2" />
|
<circle cx="10" cy="10" r="9" fill="#fff" stroke="#ef4444" stroke-width="2" />
|
||||||
@@ -186,117 +187,10 @@ const filteredTasks = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="idx < tasks.length - 1" class="task-separator"></div>
|
<div v-if="idx < tasks.length - 1" class="list-separator"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.task-listbox {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
max-width: 480px;
|
|
||||||
width: 100%;
|
|
||||||
/* Subtract any header/nav height if needed, e.g. 4.5rem */
|
|
||||||
max-height: calc(100vh - 4.5rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0.2rem 0 0 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.7rem;
|
|
||||||
background: #fff5;
|
|
||||||
padding: 0.2rem 0.2rem 0.2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.task-list-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 2px outset #38c172;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.2rem 1rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: border 0.18s;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.03);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.task-list-item.bad {
|
|
||||||
border-color: #e53e3e;
|
|
||||||
background: #fff5f5;
|
|
||||||
}
|
|
||||||
.task-list-item.good {
|
|
||||||
border-color: #38c172;
|
|
||||||
background: #f0fff4;
|
|
||||||
}
|
|
||||||
.task-image {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-right: 0.7rem;
|
|
||||||
background: #eee;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.task-name {
|
|
||||||
flex: 1;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.task-points {
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.loading,
|
|
||||||
.error,
|
|
||||||
.empty {
|
|
||||||
margin: 1.2rem 0;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #e53e3e;
|
|
||||||
}
|
|
||||||
.task-list-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.task-separator {
|
|
||||||
height: 0px;
|
|
||||||
background: #0000;
|
|
||||||
margin: 0rem 0.2rem;
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
.delete-btn {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 0.15rem;
|
|
||||||
margin-left: 0.7rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition:
|
|
||||||
background 0.15s,
|
|
||||||
box-shadow 0.15s;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
.delete-btn:hover {
|
|
||||||
background: #ffeaea;
|
|
||||||
box-shadow: 0 0 0 2px #ef444422;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.delete-btn svg {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.task-checkbox {
|
|
||||||
margin-left: 1rem;
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
accent-color: #667eea;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
<div class="modal">
|
<div class="modal">
|
||||||
<p>Are you sure you want to delete this task?</p>
|
<p>Are you sure you want to delete this task?</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="deleteTask">Yes, Delete</button>
|
<button @click="deleteTask" class="btn btn-danger">Delete</button>
|
||||||
<button @click="showConfirm = false">Cancel</button>
|
<button @click="showConfirm = false" class="btn btn-secondary">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,17 +108,20 @@ const createTask = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1.2rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 3rem; /* Increased gap for more space between buttons */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.actions button {
|
.actions button {
|
||||||
padding: 0.5rem 1.2rem;
|
padding: 1rem 2.2rem; /* Bigger touch area */
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
border: none;
|
border: 0;
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.25rem; /* Larger text */
|
||||||
|
transition: background 0.18s;
|
||||||
|
min-width: 120px; /* Ensures buttons are wide enough */
|
||||||
}
|
}
|
||||||
.actions button:first-child {
|
.actions button:first-child {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<button v-show="showBack" class="back-btn" @click="handleBack" tabindex="0">← Back</button>
|
<button v-show="showBack" class="back-btn" @click="handleBack" tabindex="0">← Back</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="login-btn">
|
<div class="login-btn-container">
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -35,28 +35,9 @@ const showBack = computed(() => route.path !== '/child')
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.layout-root {
|
/* Only keep styles unique to ChildLayout */
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar {
|
.topbar > .spacer {
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 5px 5px;
|
|
||||||
height: 48px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbar > .back-btn-container,
|
|
||||||
.topbar > .spacer,
|
|
||||||
.topbar > .login-btn {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -65,54 +46,4 @@ const showBack = computed(() => route.path !== '/child')
|
|||||||
.spacer {
|
.spacer {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
|
||||||
background: white;
|
|
||||||
border: 0;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn {
|
|
||||||
align-self: start;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.back-btn {
|
|
||||||
padding: 0.45rem 0.75rem;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login-btn button {
|
|
||||||
background: white;
|
|
||||||
border: 0;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 600;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.login-btn button {
|
|
||||||
padding: 0.45rem 0.75rem;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ onMounted(async () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<LoginButton class="login-btn" />
|
<LoginButton class="login-btn-container" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
@@ -162,35 +162,8 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.layout-root {
|
/* Only keep styles unique to ParentLayout */
|
||||||
width: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--header-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* top bar holds title and logout button */
|
|
||||||
.topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 5px 5px;
|
|
||||||
height: 48px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn-container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
flex: 1 1 0;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View Selector styles */
|
|
||||||
.view-selector {
|
.view-selector {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -230,58 +203,7 @@ onMounted(async () => {
|
|||||||
background: var(--button-hover-bg);
|
background: var(--button-hover-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* main content remains centered */
|
|
||||||
.main-content {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
box-sizing: border-box;
|
|
||||||
min-height: 0;
|
|
||||||
height: 0; /* Ensures children can use 100% height */
|
|
||||||
overflow: hidden; /* Prevents parent from scrolling */
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* back button specific styles */
|
|
||||||
.back-btn {
|
|
||||||
background: var(--button-bg);
|
|
||||||
border: 0;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--button-text);
|
|
||||||
font-weight: 600;
|
|
||||||
height: 100%; /* Make the button fill its parent */
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
align-items: center; /* Center the text/icon vertically */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1 1 0;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-version {
|
|
||||||
position: fixed;
|
|
||||||
right: 18px;
|
|
||||||
bottom: 12px;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
color: var(--app-version);
|
|
||||||
opacity: 0.85;
|
|
||||||
z-index: 100;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.back-btn,
|
|
||||||
.login-btn button,
|
|
||||||
.view-selector button {
|
.view-selector button {
|
||||||
padding: 0.45rem 0.75rem;
|
padding: 0.45rem 0.75rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import '@/assets/actions-shared.css'
|
||||||
|
import '@/assets/layout-shared.css'
|
||||||
|
import '@/assets/button-shared.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user