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:
2026-02-27 22:56:31 -08:00
parent 93e0494864
commit 417b3adfe8
68 changed files with 3919 additions and 0 deletions

View 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,
}
})

View 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 }
})

View 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,
}
})