CLAUDE: Frontend enhancements for auth and game display

Auth Improvements:
- auth.ts: Enhanced authentication store with better error handling
- auth.ts middleware: Improved redirect logic
- login.vue: Better login flow and error display

Game Display:
- PlayByPlay.vue: Enhanced play-by-play feed with player name display
- game.ts types: Additional type definitions for runner advancement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-28 12:09:39 -06:00
parent 9d0d29ef18
commit 891fb03c52
5 changed files with 136 additions and 41 deletions

View File

@ -75,6 +75,11 @@
</span>
</div>
<!-- Batter Name (if available) -->
<p v-if="getBatterName(play)" class="text-xs text-gray-500 dark:text-gray-400 mb-1 font-medium">
{{ getBatterName(play) }} batting
</p>
<!-- Play Description -->
<p
class="text-sm text-gray-900 dark:text-gray-100 font-medium leading-relaxed"
@ -83,6 +88,20 @@
{{ play.description }}
</p>
<!-- Runner Movements (if any) -->
<div v-if="hasRunnerMovements(play)" class="mt-2 space-y-0.5">
<p
v-for="(movement, idx) in getRunnerAdvancements(play)"
:key="idx"
class="text-xs text-gray-600 dark:text-gray-400 flex items-center gap-1"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<span>{{ movement }}</span>
</p>
</div>
<!-- Play Stats (Runs/Outs) -->
<div class="flex items-center gap-3 mt-3">
<!-- Runs Scored -->
@ -138,8 +157,9 @@
</template>
<script setup lang="ts">
import type { PlayResult } from '~/types/game'
import type { PlayResult, RunnerAdvancement } from '~/types/game'
import { h } from 'vue'
import { useGameStore } from '~/store/game'
interface Props {
plays?: PlayResult[]
@ -159,9 +179,60 @@ const props = withDefaults(defineProps<Props>(), {
showFilters: true
})
// Store for player name lookup
const gameStore = useGameStore()
// State
const showAllPlays = ref(false)
// Helper functions for player names
const getPlayerName = (lineupId: number | undefined): string | null => {
if (!lineupId) return null
const lineup = gameStore.findPlayerInLineup(lineupId)
return lineup?.player?.name || null
}
const getBatterName = (play: PlayResult): string | null => {
return getPlayerName(play.batter_lineup_id)
}
const formatBaseName = (base: number): string => {
switch (base) {
case 0: return 'Home'
case 1: return '1st'
case 2: return '2nd'
case 3: return '3rd'
case 4: return 'Home'
default: return `${base}B`
}
}
const formatRunnerAdvancement = (adv: RunnerAdvancement): string => {
const playerName = getPlayerName(adv.lineup_id)
const name = playerName || `R${adv.from}`
const from = formatBaseName(adv.from)
const to = adv.to === 4 ? 'scores' : formatBaseName(adv.to)
if (adv.is_out) {
return `${name} out at ${to}`
} else if (adv.to === 4) {
return `${name} ${to}`
} else {
return `${name}: ${from}${to}`
}
}
const getRunnerAdvancements = (play: PlayResult): string[] => {
if (!play.runners_advanced || play.runners_advanced.length === 0) {
return []
}
return play.runners_advanced.map(formatRunnerAdvancement)
}
const hasRunnerMovements = (play: PlayResult): boolean => {
return play.runners_advanced && play.runners_advanced.length > 0
}
// Computed
const displayedPlays = computed(() => {
if (!props.plays || props.plays.length === 0) return []
@ -175,9 +246,9 @@ const displayedPlays = computed(() => {
// Methods
const formatInning = (play: PlayResult): string => {
// Extract inning from play (assuming it's in description or metadata)
// Fallback format for now
return `Inning ${play.inning || '?'}`
if (!play.inning) return 'Inning ?'
const half = play.half === 'top' ? 'Top' : play.half === 'bottom' ? 'Bot' : ''
return half ? `${half} ${play.inning}` : `Inning ${play.inning}`
}
const formatOutcome = (outcome: string): string => {

View File

@ -4,27 +4,39 @@
* Protects routes that require authentication.
* Redirects to login if user is not authenticated.
*
* Note: Auth state is initialized by the auth.client.ts plugin before this middleware runs.
* Works on both server and client:
* - Server: Forwards cookies from incoming request to /api/auth/me
* - Client: Browser sends cookies automatically
*/
import { useAuthStore } from '~/store/auth'
export default defineNuxtRouteMiddleware((to, from) => {
export default defineNuxtRouteMiddleware(async (to, from) => {
const authStore = useAuthStore()
// If already authenticated (from previous navigation), allow access
if (authStore.isAuthenticated) {
console.log('[Auth Middleware] Already authenticated, allowing access to:', to.path)
return
}
// Check auth status by calling backend
console.log('[Auth Middleware] Checking auth for:', to.path)
const isAuthed = await authStore.checkAuth()
console.log('[Auth Middleware]', {
path: to.path,
isAuthenticated: authStore.isAuthenticated,
isAuthenticated: isAuthed,
hasUser: !!authStore.currentUser,
})
// Allow access if authenticated
if (authStore.isAuthenticated) {
if (isAuthed) {
return
}
// Redirect to login with return URL
console.log('[Auth Middleware] Redirecting to login')
console.log('[Auth Middleware] Not authenticated, redirecting to login')
return navigateTo({
path: '/auth/login',
query: { redirect: to.fullPath },

View File

@ -38,14 +38,12 @@
</div>
</div>
<!-- Discord Login Button -->
<button
:disabled="isLoading"
class="w-full flex items-center justify-center space-x-3 px-6 py-4 bg-[#5865F2] hover:bg-[#4752C4] disabled:bg-gray-400 text-white font-semibold rounded-lg transition shadow-lg hover:shadow-xl disabled:cursor-not-allowed"
@click="handleDiscordLogin"
<!-- Discord Login Button (anchor tag for proxy/iPad compatibility) -->
<a
:href="discordLoginUrl"
class="w-full flex items-center justify-center space-x-3 px-6 py-4 bg-[#5865F2] hover:bg-[#4752C4] text-white font-semibold rounded-lg transition shadow-lg hover:shadow-xl no-underline"
>
<svg
v-if="!isLoading"
class="w-6 h-6"
viewBox="0 0 24 24"
fill="currentColor"
@ -54,12 +52,8 @@
d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515a.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0a12.64 12.64 0 00-.617-1.25a.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057a19.9 19.9 0 005.993 3.03a.078.078 0 00.084-.028a14.09 14.09 0 001.226-1.994a.076.076 0 00-.041-.106a13.107 13.107 0 01-1.872-.892a.077.077 0 01-.008-.128a10.2 10.2 0 00.372-.292a.074.074 0 01.077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127a12.299 12.299 0 01-1.873.892a.077.077 0 00-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028a19.839 19.839 0 006.002-3.03a.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>
<div
v-if="isLoading"
class="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin"
/>
<span>{{ isLoading ? 'Connecting...' : 'Continue with Discord' }}</span>
</button>
<span>Continue with Discord</span>
</a>
<!-- Additional Info -->
<div class="mt-6 text-center text-sm text-gray-600">
@ -95,10 +89,16 @@ definePageMeta({
const authStore = useAuthStore()
const route = useRoute()
const router = useRouter()
const config = useRuntimeConfig()
const isLoading = ref(false)
const error = ref<string | null>(null)
// Compute Discord login URL directly (no JavaScript click handler needed)
const discordLoginUrl = computed(() => {
const returnUrl = (route.query.redirect as string) || '/'
return `${config.public.apiUrl}/api/auth/discord/login?return_url=${encodeURIComponent(returnUrl)}`
})
// Check if already authenticated
onMounted(() => {
if (authStore.isAuthenticated) {
@ -107,20 +107,6 @@ onMounted(() => {
router.push(redirect)
}
})
const handleDiscordLogin = () => {
try {
isLoading.value = true
error.value = null
// Trigger Discord OAuth flow (will redirect to Discord)
authStore.loginWithDiscord()
} catch (err: any) {
isLoading.value = false
error.value = err.message || 'Failed to initiate login. Please try again.'
console.error('Login error:', err)
}
}
</script>
<style scoped>

View File

@ -45,18 +45,38 @@ export const useAuthStore = defineStore('auth', () => {
try {
const config = useRuntimeConfig()
const url = `${config.public.apiUrl}/api/auth/me`
console.log('[Auth Store] checkAuth() calling:', url)
// Get cookies from incoming request (for SSR) or use browser cookies (for client)
const headers: Record<string, string> = {}
if (import.meta.server) {
// Server-side: forward cookies from incoming request
const event = useRequestEvent()
const cookieHeader = event?.node.req.headers.cookie
if (cookieHeader) {
headers['Cookie'] = cookieHeader
console.log('[Auth Store] SSR: Forwarding cookies')
} else {
console.log('[Auth Store] SSR: No cookies to forward')
}
}
const response = await $fetch<{
user: DiscordUser
teams: Team[]
}>(`${config.public.apiUrl}/api/auth/me`, {
credentials: 'include', // Send cookies
}>(url, {
credentials: 'include', // Send cookies (client-side)
headers, // Forward cookies (server-side)
})
console.log('[Auth Store] checkAuth() success:', response.user?.username)
user.value = response.user
teams.value = response.teams
return true
} catch (err: any) {
// Not authenticated or token expired
console.log('[Auth Store] checkAuth() failed:', err.message || err)
user.value = null
teams.value = []
return false

View File

@ -212,9 +212,10 @@ export type PlayOutcome =
* Runner advancement during a play
*/
export interface RunnerAdvancement {
from: number // 0=batter, 1-3=bases
to: number // 1-4=bases (4=home/scored)
out?: boolean // Runner was out during advancement
from: number // 0=batter, 1-3=bases
to: number // 1-4=bases (4=home/scored)
lineup_id: number // Player's lineup ID for name lookup
is_out?: boolean // Runner was out during advancement (renamed from 'out')
}
/**
@ -224,6 +225,8 @@ export interface RunnerAdvancement {
export interface PlayResult {
// Play identification
play_number: number
inning?: number // Inning when play occurred
half?: InningHalf // 'top' or 'bottom'
outcome: PlayOutcome
// Play description
@ -234,6 +237,9 @@ export interface PlayResult {
outs_recorded: number
runs_scored: number
// Player identification for display
batter_lineup_id?: number // Batter's lineup ID for name lookup
// Runner advancement
runners_advanced: RunnerAdvancement[]
batter_result: number | null // Where batter ended up (1-4, null=out)