Initial project scaffold
Full-stack homeschool web app with FastAPI backend, Vue 3 frontend, MySQL database, and Docker Compose orchestration. Includes JWT auth, WebSocket real-time TV dashboard, schedule builder, activity logging, and multi-child support. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
76
frontend/src/stores/auth.js
Normal file
76
frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/composables/useApi'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessToken = ref(localStorage.getItem('access_token') || null)
|
||||
const user = ref(null)
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value)
|
||||
|
||||
function setToken(token) {
|
||||
accessToken.value = token
|
||||
localStorage.setItem('access_token', token)
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
accessToken.value = null
|
||||
user.value = null
|
||||
localStorage.removeItem('access_token')
|
||||
}
|
||||
|
||||
async function login(email, password) {
|
||||
const res = await api.post('/api/auth/login', { email, password })
|
||||
setToken(res.data.access_token)
|
||||
await fetchMe()
|
||||
}
|
||||
|
||||
async function register(email, password, fullName) {
|
||||
const res = await api.post('/api/auth/register', {
|
||||
email,
|
||||
password,
|
||||
full_name: fullName,
|
||||
})
|
||||
setToken(res.data.access_token)
|
||||
await fetchMe()
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await api.post('/api/auth/logout')
|
||||
} catch (_) {
|
||||
// ignore errors on logout
|
||||
}
|
||||
clearToken()
|
||||
}
|
||||
|
||||
async function tryRefresh() {
|
||||
try {
|
||||
const res = await api.post('/api/auth/refresh')
|
||||
setToken(res.data.access_token)
|
||||
await fetchMe()
|
||||
} catch (_) {
|
||||
clearToken()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMe() {
|
||||
try {
|
||||
const res = await api.get('/api/users/me')
|
||||
user.value = res.data
|
||||
} catch (_) {
|
||||
clearToken()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
user,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
tryRefresh,
|
||||
fetchMe,
|
||||
}
|
||||
})
|
||||
40
frontend/src/stores/children.js
Normal file
40
frontend/src/stores/children.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/composables/useApi'
|
||||
|
||||
export const useChildrenStore = defineStore('children', () => {
|
||||
const children = ref([])
|
||||
const activeChild = ref(null)
|
||||
|
||||
async function fetchChildren() {
|
||||
const res = await api.get('/api/children')
|
||||
children.value = res.data
|
||||
if (!activeChild.value && children.value.length > 0) {
|
||||
activeChild.value = children.value[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function createChild(data) {
|
||||
const res = await api.post('/api/children', data)
|
||||
children.value.push(res.data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function updateChild(id, data) {
|
||||
const res = await api.patch(`/api/children/${id}`, data)
|
||||
const idx = children.value.findIndex((c) => c.id === id)
|
||||
if (idx !== -1) children.value[idx] = res.data
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function deleteChild(id) {
|
||||
await api.delete(`/api/children/${id}`)
|
||||
children.value = children.value.filter((c) => c.id !== id)
|
||||
}
|
||||
|
||||
function setActiveChild(child) {
|
||||
activeChild.value = child
|
||||
}
|
||||
|
||||
return { children, activeChild, fetchChildren, createChild, updateChild, deleteChild, setActiveChild }
|
||||
})
|
||||
79
frontend/src/stores/schedule.js
Normal file
79
frontend/src/stores/schedule.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/composables/useApi'
|
||||
|
||||
export const useScheduleStore = defineStore('schedule', () => {
|
||||
const session = ref(null)
|
||||
const blocks = ref([])
|
||||
const completedBlockIds = ref([])
|
||||
const child = ref(null)
|
||||
|
||||
const currentBlock = computed(() =>
|
||||
session.value?.current_block_id
|
||||
? blocks.value.find((b) => b.id === session.value.current_block_id) || null
|
||||
: null
|
||||
)
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!blocks.value.length) return 0
|
||||
return Math.round((completedBlockIds.value.length / blocks.value.length) * 100)
|
||||
})
|
||||
|
||||
function applySnapshot(snapshot) {
|
||||
session.value = snapshot.session
|
||||
blocks.value = snapshot.blocks || []
|
||||
completedBlockIds.value = snapshot.completed_block_ids || []
|
||||
if (snapshot.child) child.value = snapshot.child
|
||||
}
|
||||
|
||||
function applyWsEvent(event) {
|
||||
if (event.event === 'session_update') {
|
||||
applySnapshot(event)
|
||||
return
|
||||
}
|
||||
// Timer events update session state
|
||||
if (event.current_block_id !== undefined && session.value) {
|
||||
session.value.current_block_id = event.current_block_id
|
||||
}
|
||||
if (event.event === 'complete' && event.block_id) {
|
||||
if (!completedBlockIds.value.includes(event.block_id)) {
|
||||
completedBlockIds.value.push(event.block_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDashboard(childId) {
|
||||
const res = await api.get(`/api/dashboard/${childId}`)
|
||||
applySnapshot(res.data)
|
||||
}
|
||||
|
||||
async function startSession(childId, templateId) {
|
||||
const res = await api.post('/api/sessions', {
|
||||
child_id: childId,
|
||||
template_id: templateId,
|
||||
})
|
||||
session.value = res.data
|
||||
completedBlockIds.value = []
|
||||
}
|
||||
|
||||
async function sendTimerAction(sessionId, eventType, blockId = null) {
|
||||
await api.post(`/api/sessions/${sessionId}/timer`, {
|
||||
event_type: eventType,
|
||||
block_id: blockId,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
blocks,
|
||||
completedBlockIds,
|
||||
child,
|
||||
currentBlock,
|
||||
progressPercent,
|
||||
applySnapshot,
|
||||
applyWsEvent,
|
||||
fetchDashboard,
|
||||
startSession,
|
||||
sendTimerAction,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user