From a89d3d7313b699bfd26938527d82de2562845017 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Sat, 10 Jan 2026 22:19:23 -0500 Subject: [PATCH] starting refactor styling --- api/auth_api.py | 109 +++++------ api/user_api.py | 45 +++++ main.py | 11 +- models/user.py | 50 ++++++ web/vue-app/src/assets/button-shared.css | 49 ++++- web/vue-app/src/assets/child-list-shared.css | 9 - web/vue-app/src/assets/common.css | 29 +++ web/vue-app/src/assets/edit-forms.css | 55 ++++-- web/vue-app/src/assets/global.css | 4 +- web/vue-app/src/assets/list-shared.css | 51 ------ web/vue-app/src/assets/modal.css | 25 +++ web/vue-app/src/assets/scroll.css | 25 +++ web/vue-app/src/assets/view-shared.css | 15 -- .../src/components/child/ChildEditView.vue | 52 +----- .../src/components/profile/UserProfile.vue | 169 ++++++++++++++++++ .../src/components/reward/RewardEditView.vue | 32 ++-- .../components/shared/ChildrenListView.vue | 96 ++-------- .../src/components/shared/ItemList.vue | 163 +++++++++++++++++ .../src/components/shared/LoginButton.vue | 36 ++-- .../src/components/shared/MessageBlock.vue | 28 +++ .../src/components/task/TaskEditView.vue | 77 +++++--- web/vue-app/src/components/task/TaskView.vue | 15 +- .../src/components/utils/ImagePicker.vue | 9 +- web/vue-app/src/router/index.ts | 5 + 24 files changed, 815 insertions(+), 344 deletions(-) create mode 100644 api/user_api.py create mode 100644 models/user.py create mode 100644 web/vue-app/src/assets/common.css create mode 100644 web/vue-app/src/assets/modal.css create mode 100644 web/vue-app/src/assets/scroll.css create mode 100644 web/vue-app/src/components/profile/UserProfile.vue create mode 100644 web/vue-app/src/components/shared/ItemList.vue create mode 100644 web/vue-app/src/components/shared/MessageBlock.vue diff --git a/api/auth_api.py b/api/auth_api.py index 9af136e..3b93832 100644 --- a/api/auth_api.py +++ b/api/auth_api.py @@ -1,7 +1,7 @@ import logging import secrets, jwt from datetime import datetime, timedelta, timezone - +from models.user import User from flask import Blueprint, request, jsonify, current_app from flask_mail import Mail, Message from tinydb import Query @@ -57,15 +57,17 @@ def signup(): token = secrets.token_urlsafe(32) now_iso = datetime.utcnow().isoformat() - users_db.insert({ - 'first_name': data['first_name'], - 'last_name': data['last_name'], - 'email': data['email'], - 'password': data['password'], # Hash in production! - 'verified': False, - 'verify_token': token, - 'verify_token_created': now_iso - }) + user = User( + first_name=data['first_name'], + last_name=data['last_name'], + email=data['email'], + password=data['password'], # Hash in production! + verified=False, + verify_token=token, + verify_token_created=now_iso, + image_id="boy01" + ) + users_db.insert(user.to_dict()) send_verification_email(data['email'], token) return jsonify({'message': 'User created, verification email sent'}), 201 @@ -75,6 +77,7 @@ def verify(): status = 'success' reason = '' code = '' + user_dict = None user = None if not token: @@ -82,13 +85,14 @@ def verify(): reason = 'Missing token' code = MISSING_TOKEN else: - user = users_db.get(Query().verify_token == token) + user_dict = users_db.get(Query().verify_token == token) + user = User.from_dict(user_dict) if user_dict else None if not user: status = 'error' reason = 'Invalid token' code = INVALID_TOKEN else: - created_str = user.get('verify_token_created') + created_str = user.verify_token_created if not created_str: status = 'error' reason = 'Token timestamp missing' @@ -100,14 +104,17 @@ def verify(): reason = 'Token expired' code = TOKEN_EXPIRED else: - users_db.update({'verified': True, 'verify_token': None, 'verify_token_created': None}, Query().verify_token == token) + user.verified = True + user.verify_token = None + user.verify_token_created = None + users_db.update(user.to_dict(), Query().email == user.email) http_status = 200 if status == 'success' else 400 - if http_status == 200 and user is not None: ##user is verified, create the user's image directory - if 'email' not in user: + if http_status == 200 and user is not None: + if not user.email: logger.error("Verified user has no email field.") else: - user_image_dir = get_user_image_dir(sanitize_email(user['email'])) + user_image_dir = get_user_image_dir(sanitize_email(user.email)) os.makedirs(user_image_dir, exist_ok=True) return jsonify({'status': status, 'reason': reason, 'code': code}), http_status @@ -119,23 +126,22 @@ def resend_verify(): if not email: return jsonify({'error': 'Missing email', 'code': MISSING_EMAIL}), 400 - user = users_db.get(UserQuery.email == email) + user_dict = users_db.get(UserQuery.email == email) + user = User.from_dict(user_dict) if user_dict else None if not user: return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 - if user.get('verified'): + if user.verified: return jsonify({'error': 'Account already verified', 'code': ALREADY_VERIFIED}), 400 token = secrets.token_urlsafe(32) now_iso = datetime.utcnow().isoformat() - users_db.update({ - 'verify_token': token, - 'verify_token_created': now_iso - }, UserQuery.email == email) + user.verify_token = token + user.verify_token_created = now_iso + users_db.update(user.to_dict(), UserQuery.email == email) send_verification_email(email, token) return jsonify({'message': 'Verification email resent'}), 200 - @auth_api.route('/login', methods=['POST']) def login(): data = request.get_json() @@ -144,11 +150,12 @@ def login(): if not email or not password: return jsonify({'error': 'Missing email or password', 'code': MISSING_EMAIL_OR_PASSWORD}), 400 - user = users_db.get(UserQuery.email == email) - if not user or user.get('password') != password: + user_dict = users_db.get(UserQuery.email == email) + user = User.from_dict(user_dict) if user_dict else None + if not user or user.password != password: return jsonify({'error': 'Invalid credentials', 'code': INVALID_CREDENTIALS}), 401 - if not user.get('verified'): + if not user.verified: return jsonify({'error': 'This account has not verified', 'code': NOT_VERIFIED}), 403 payload = { @@ -170,45 +177,39 @@ def me(): try: payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) email = payload.get('email') - user = users_db.get(UserQuery.email == email) + user_dict = users_db.get(UserQuery.email == email) + user = User.from_dict(user_dict) if user_dict else None if not user: return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 return jsonify({ - 'email': user['email'], - 'id': sanitize_email(user['email']), - 'first_name': user['first_name'], - 'last_name': user['last_name'], - 'verified': user['verified'] + 'email': user.email, + 'id': sanitize_email(user.email), + 'first_name': user.first_name, + 'last_name': user.last_name, + 'verified': user.verified }), 200 except jwt.ExpiredSignatureError: return jsonify({'error': 'Token expired', 'code': TOKEN_EXPIRED}), 401 except jwt.InvalidTokenError: return jsonify({'error': 'Invalid token', 'code': INVALID_TOKEN}), 401 -@auth_api.route('/logout', methods=['POST']) -def logout(): - resp = jsonify({'message': 'Logged out'}) - resp.set_cookie('token', '', expires=0, httponly=True, secure=True, samesite='Strict') - return resp, 200 - @auth_api.route('/request-password-reset', methods=['POST']) def request_password_reset(): data = request.get_json() email = data.get('email') - # Always return success for privacy success_msg = 'If this email is registered, you will receive a password reset link shortly.' if not email: return jsonify({'error': 'Missing email', 'code': MISSING_EMAIL}), 400 - user = users_db.get(UserQuery.email == email) + user_dict = users_db.get(UserQuery.email == email) + user = User.from_dict(user_dict) if user_dict else None if user: token = secrets.token_urlsafe(32) now_iso = datetime.utcnow().isoformat() - users_db.update({ - 'reset_token': token, - 'reset_token_created': now_iso - }, UserQuery.email == email) + user.reset_token = token + user.reset_token_created = now_iso + users_db.update(user.to_dict(), UserQuery.email == email) send_reset_password_email(email, token) return jsonify({'message': success_msg}), 200 @@ -219,11 +220,12 @@ def validate_reset_token(): if not token: return jsonify({'error': 'Missing token', 'code': MISSING_TOKEN}), 400 - user = users_db.get(UserQuery.reset_token == token) + user_dict = users_db.get(UserQuery.reset_token == token) + user = User.from_dict(user_dict) if user_dict else None if not user: return jsonify({'error': 'Invalid token', 'code': INVALID_TOKEN}), 400 - created_str = user.get('reset_token_created') + created_str = user.reset_token_created if not created_str: return jsonify({'error': 'Token timestamp missing', 'code': TOKEN_TIMESTAMP_MISSING}), 400 @@ -242,11 +244,12 @@ def reset_password(): if not token or not new_password: return jsonify({'error': 'Missing token or password'}), 400 - user = users_db.get(UserQuery.reset_token == token) + user_dict = users_db.get(UserQuery.reset_token == token) + user = User.from_dict(user_dict) if user_dict else None if not user: return jsonify({'error': 'Invalid token', 'code': INVALID_TOKEN}), 400 - created_str = user.get('reset_token_created') + created_str = user.reset_token_created if not created_str: return jsonify({'error': 'Token timestamp missing', 'code': TOKEN_TIMESTAMP_MISSING}), 400 @@ -254,11 +257,9 @@ def reset_password(): if datetime.now(timezone.utc) - created_dt > timedelta(minutes=RESET_PASSWORD_TOKEN_EXPIRY_MINUTES): return jsonify({'error': 'Token expired', 'code': TOKEN_EXPIRED}), 400 - users_db.update({ - 'password': new_password, # Hash in production! - 'reset_token': None, - 'reset_token_created': None - }, UserQuery.reset_token == token) + user.password = new_password # Hash in production! + user.reset_token = None + user.reset_token_created = None + users_db.update(user.to_dict(), UserQuery.email == user.email) return jsonify({'message': 'Password has been reset'}), 200 - diff --git a/api/user_api.py b/api/user_api.py new file mode 100644 index 0000000..9e464df --- /dev/null +++ b/api/user_api.py @@ -0,0 +1,45 @@ +from flask import Blueprint, request, jsonify, current_app +from models.user import User +from tinydb import Query +from db.db import users_db +import jwt + +user_api = Blueprint('user_api', __name__) +UserQuery = Query() + +def get_current_user(): + token = request.cookies.get('token') + if not token: + return None + try: + payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256']) + email = payload.get('email') + user_dict = users_db.get(UserQuery.email == email) + return User.from_dict(user_dict) if user_dict else None + except Exception: + return None + +@user_api.route('/user/profile', methods=['GET']) +def get_profile(): + user = get_current_user() + if not user: + return jsonify({'error': 'Unauthorized'}), 401 + return jsonify({ + 'first_name': user.first_name, + 'last_name': user.last_name, + 'email': user.email, + 'image_id': user.image_id + }), 200 + +@user_api.route('/user/image', methods=['PUT']) +def update_image(): + user = get_current_user() + if not user: + return jsonify({'error': 'Unauthorized'}), 401 + data = request.get_json() + image_id = data.get('image_id') + if not image_id: + return jsonify({'error': 'Missing image_id'}), 400 + user.image_id = image_id + users_db.update(user.to_dict(), UserQuery.email == user.email) + return jsonify({'message': 'Image updated', 'image_id': image_id}), 200 diff --git a/main.py b/main.py index cef9f61..ed11933 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,19 @@ -import sys, logging, os -from config.paths import get_user_image_dir +import logging +import sys + from flask import Flask, request, jsonify from flask_cors import CORS +from api.auth_api import auth_api, mail from api.child_api import child_api from api.image_api import image_api from api.reward_api import reward_api from api.task_api import task_api -from api.auth_api import auth_api, mail +from api.user_api import user_api from config.version import get_full_version +from db.default import initializeImages, createDefaultTasks, createDefaultRewards from events.broadcaster import Broadcaster from events.sse import sse_response_for_user, send_to_user -from db.default import initializeImages, createDefaultTasks, createDefaultRewards # Configure logging once at application startup logging.basicConfig( @@ -30,6 +32,7 @@ app.register_blueprint(reward_api) app.register_blueprint(task_api) app.register_blueprint(image_api) app.register_blueprint(auth_api) +app.register_blueprint(user_api) app.config.update( MAIL_SERVER='smtp.gmail.com', diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..25f74a0 --- /dev/null +++ b/models/user.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass, field +from models.base import BaseModel + +@dataclass +class User(BaseModel): + first_name: str + last_name: str + email: str + password: str # In production, this should be hashed + verified: bool = False + verify_token: str | None = None + verify_token_created: str | None = None + reset_token: str | None = None + reset_token_created: str | None = None + image_id: str | None = None + + @classmethod + def from_dict(cls, d: dict): + return cls( + first_name=d.get('first_name'), + last_name=d.get('last_name'), + email=d.get('email'), + password=d.get('password'), + verified=d.get('verified', False), + verify_token=d.get('verify_token'), + verify_token_created=d.get('verify_token_created'), + reset_token=d.get('reset_token'), + reset_token_created=d.get('reset_token_created'), + image_id=d.get('image_id'), + id=d.get('id'), + created_at=d.get('created_at'), + updated_at=d.get('updated_at') + + ) + + def to_dict(self): + base = super().to_dict() + base.update({ + 'first_name': self.first_name, + 'last_name': self.last_name, + 'email': self.email, + 'password': self.password, + 'verified': self.verified, + 'verify_token': self.verify_token, + 'verify_token_created': self.verify_token_created, + 'reset_token': self.reset_token, + 'reset_token_created': self.reset_token_created, + 'image_id': self.image_id + }) + return base diff --git a/web/vue-app/src/assets/button-shared.css b/web/vue-app/src/assets/button-shared.css index d88ef28..e663148 100644 --- a/web/vue-app/src/assets/button-shared.css +++ b/web/vue-app/src/assets/button-shared.css @@ -93,10 +93,57 @@ font-weight: 600; margin-left: 6px; } -.btn-link.btn-disabled { +.btn-link:disabled { text-decoration: none; opacity: 0.75; cursor: default; pointer-events: none; color: var(--btn-primary); } + +.round-btn { + background: var(--sign-in-btn-bg); + color: var(--sign-in-btn-color); + border: 2px solid var(--sign-in-btn-border); + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + padding: 0.2rem 0.5rem; + margin-right: 0.1rem; + cursor: pointer; + transition: + background 0.18s, + color 0.18s; +} +.round-btn:hover { + background: var(--sign-in-btn-hover-bg); + color: var(--sign-in-btn-hover-color); +} + +/* Floating Action Button (FAB) */ +.fab { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--fab-bg); + color: #fff; + border: none; + border-radius: 50%; + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + cursor: pointer; + font-size: 24px; + z-index: 1300; +} + +.fab:hover { + background: var(--fab-hover-bg); +} + +.fab:active { + background: var(--fab-active-bg); +} diff --git a/web/vue-app/src/assets/child-list-shared.css b/web/vue-app/src/assets/child-list-shared.css index bd6c071..7186cd6 100644 --- a/web/vue-app/src/assets/child-list-shared.css +++ b/web/vue-app/src/assets/child-list-shared.css @@ -14,15 +14,6 @@ color: var(--child-list-title-color, #fff); } -.loading, -.empty { - text-align: center; - padding: 1rem; - font-size: 0.9rem; - opacity: 0.8; - color: var(--child-list-loading-color, #fff); -} - .scroll-wrapper { overflow-x: auto; overflow-y: hidden; diff --git a/web/vue-app/src/assets/common.css b/web/vue-app/src/assets/common.css new file mode 100644 index 0000000..1a97935 --- /dev/null +++ b/web/vue-app/src/assets/common.css @@ -0,0 +1,29 @@ +.loading, +.empty { + text-align: center; + padding: 1rem; + font-size: 0.9rem; + opacity: 0.8; + color: var(--child-list-loading-color, #fff); +} + +.error { + color: var(--error); + margin-top: 0.7rem; + text-align: center; + background: var(--error-bg); + border-radius: 8px; + padding: 1rem; +} + +.error-message { + color: var(--error, #e53e3e); + font-size: 0.98rem; + margin-top: 0.4rem; + display: block; +} + +.success-message { + color: var(--success, #16a34a); + font-size: 1rem; +} diff --git a/web/vue-app/src/assets/edit-forms.css b/web/vue-app/src/assets/edit-forms.css index f71f923..3e7d815 100644 --- a/web/vue-app/src/assets/edit-forms.css +++ b/web/vue-app/src/assets/edit-forms.css @@ -1,3 +1,4 @@ +.profile-view, .edit-view, .child-edit-view, .reward-edit-view, @@ -10,6 +11,7 @@ padding: 2rem 2.2rem 1.5rem 2.2rem; } +.profile-view h2, .edit-view h2, .child-edit-view h2, .reward-edit-view h2, @@ -19,22 +21,38 @@ color: var(--form-heading); } -.form-group, -.reward-form label, -.task-form label { - display: block; - margin-bottom: 1.1rem; - font-weight: 500; - color: var(--form-label); - width: 100%; +.profile-form, +.task-form, +.reward-form, +.child-edit-form { + display: flex; + flex-direction: column; + gap: 1.1rem; } -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'] { +.profile-form div.group, +.task-form div.group, +.reward-form div.group, +.child-edit-form div.group { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.35rem; + box-sizing: border-box; +} + +.profile-form div.group label, +.task-form div.group label, +.reward-form div.group label, +.child-edit-form div.group label { + font-weight: 600; + color: var(--form-label-color); + font-size: 1rem; +} + +div.group input[type='text'], +div.group input[type='number'], +div.group input[type='email'] { display: block; width: 100%; margin-top: 0.4rem; @@ -45,9 +63,18 @@ input[type='number'], background: var(--form-input-bg); box-sizing: border-box; } +div.group input:focus { + outline: none; + border: 1.5px solid var(--form-input-focus); +} .loading-message { text-align: center; color: var(--form-loading); margin-bottom: 1.2rem; } + +.form-group.image-picker-group { + display: block; + text-align: left; +} diff --git a/web/vue-app/src/assets/global.css b/web/vue-app/src/assets/global.css index 6e30c93..65524c9 100644 --- a/web/vue-app/src/assets/global.css +++ b/web/vue-app/src/assets/global.css @@ -71,8 +71,8 @@ --fab-bg: #667eea; --fab-hover-bg: #5a67d8; --fab-active-bg: #4c51bf; - --no-children-color: #fdfdfd; - --sub-message-color: #5d719d; + --message-block-color: #fdfdfd; + --sub-message-color: #c1d0f1; --sign-in-btn-bg: #fff; --sign-in-btn-color: #2563eb; --sign-in-btn-border: #2563eb; diff --git a/web/vue-app/src/assets/list-shared.css b/web/vue-app/src/assets/list-shared.css index 3142b7f..a2b0808 100644 --- a/web/vue-app/src/assets/list-shared.css +++ b/web/vue-app/src/assets/list-shared.css @@ -14,57 +14,6 @@ border-radius: 12px; } -/* List item */ -.list-item { - display: flex; - align-items: center; - border: 2px outset var(--list-item-border-good); - border-radius: 8px; - padding: 0.2rem 1rem; - background: var(--list-item-bg); - 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: var(--list-item-border-bad); - background: var(--list-item-bg-bad); -} -.list-item.good { - border-color: var(--list-item-border-good); - background: var(--list-item-bg-good); -} - -/* Image styles */ -.list-image { - width: 36px; - height: 36px; - object-fit: cover; - border-radius: 8px; - margin-right: 0.7rem; - background: var(--list-image-bg); - 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; diff --git a/web/vue-app/src/assets/modal.css b/web/vue-app/src/assets/modal.css new file mode 100644 index 0000000..d3c51b8 --- /dev/null +++ b/web/vue-app/src/assets/modal.css @@ -0,0 +1,25 @@ +.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: var(--modal-bg); + color: var(--modal-text); + padding: 1.25rem; + border-radius: 10px; + width: 360px; + max-width: calc(100% - 32px); + box-shadow: var(--modal-shadow); + text-align: center; +} + +.modal h3 { + margin-bottom: 0.5rem; + font-size: 1.05rem; +} diff --git a/web/vue-app/src/assets/scroll.css b/web/vue-app/src/assets/scroll.css new file mode 100644 index 0000000..14c4429 --- /dev/null +++ b/web/vue-app/src/assets/scroll.css @@ -0,0 +1,25 @@ +.scroll-wrapper::-webkit-scrollbar { + height: 8px; +} + +.scroll-wrapper::-webkit-scrollbar-track { + background: var(--child-list-scrollbar-track, rgba(255, 255, 255, 0.05)); + border-radius: 10px; +} + +.scroll-wrapper::-webkit-scrollbar-thumb { + background: var( + --child-list-scrollbar-thumb, + linear-gradient(180deg, rgba(102, 126, 234, 0.8), rgba(118, 75, 162, 0.8)) + ); + border-radius: 10px; + border: var(--child-list-scrollbar-thumb-border, 2px solid rgba(255, 255, 255, 0.08)); +} + +.scroll-wrapper::-webkit-scrollbar-thumb:hover { + background: var( + --child-list-scrollbar-thumb-hover, + linear-gradient(180deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1)) + ); + box-shadow: 0 0 8px rgba(102, 126, 234, 0.4); +} diff --git a/web/vue-app/src/assets/view-shared.css b/web/vue-app/src/assets/view-shared.css index 47008ae..2121b6d 100644 --- a/web/vue-app/src/assets/view-shared.css +++ b/web/vue-app/src/assets/view-shared.css @@ -94,21 +94,6 @@ font-size: 1rem; } -.no-message { - margin: 2rem 0; - font-size: 1.15rem; - font-weight: 600; - text-align: center; - color: #fdfdfd; - line-height: 1.5; -} -.sub-message { - margin-top: 0.3rem; - font-size: 1rem; - font-weight: 400; - color: #b5ccff; -} - /* Floating Action Button (FAB) */ .fab { position: fixed; diff --git a/web/vue-app/src/components/child/ChildEditView.vue b/web/vue-app/src/components/child/ChildEditView.vue index 9f29bc2..6971a4e 100644 --- a/web/vue-app/src/components/child/ChildEditView.vue +++ b/web/vue-app/src/components/child/ChildEditView.vue @@ -2,16 +2,16 @@

{{ isEdit ? 'Edit Child' : 'Create Child' }}

Loading child...
-
-
+ +
- +
-
+
-
+
- + diff --git a/web/vue-app/src/components/profile/UserProfile.vue b/web/vue-app/src/components/profile/UserProfile.vue new file mode 100644 index 0000000..22ee309 --- /dev/null +++ b/web/vue-app/src/components/profile/UserProfile.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/web/vue-app/src/components/reward/RewardEditView.vue b/web/vue-app/src/components/reward/RewardEditView.vue index b57cc3b..f376bad 100644 --- a/web/vue-app/src/components/reward/RewardEditView.vue +++ b/web/vue-app/src/components/reward/RewardEditView.vue @@ -3,19 +3,25 @@

{{ isEdit ? 'Edit Reward' : 'Create Reward' }}

Loading reward...
- - - -
+
+ +
+
+ +
+
+ +
+
([]) @@ -273,16 +277,13 @@ onBeforeUnmount(() => { diff --git a/web/vue-app/src/components/shared/ItemList.vue b/web/vue-app/src/components/shared/ItemList.vue new file mode 100644 index 0000000..89b1fdc --- /dev/null +++ b/web/vue-app/src/components/shared/ItemList.vue @@ -0,0 +1,163 @@ + + + + diff --git a/web/vue-app/src/components/shared/LoginButton.vue b/web/vue-app/src/components/shared/LoginButton.vue index 9ec64cc..515525d 100644 --- a/web/vue-app/src/components/shared/LoginButton.vue +++ b/web/vue-app/src/components/shared/LoginButton.vue @@ -3,6 +3,8 @@ import { ref, nextTick, onMounted, onUnmounted } from 'vue' import { useRouter } from 'vue-router' import { eventBus } from '@/common/eventBus' import { authenticateParent, isParentAuthenticated, logoutParent } from '../../stores/auth' +import '@/assets/modal.css' +import '@/assets/actions-shared.css' const router = useRouter() const show = ref(false) @@ -62,6 +64,10 @@ async function signOut() { dropdownOpen.value = false } +function goToProfile() { + router.push('/parent/profile') +} + onMounted(() => { eventBus.on('open-login', open) }) @@ -71,7 +77,7 @@ onUnmounted(() => {