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:
parent
9d0d29ef18
commit
891fb03c52
@ -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 => {
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user