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>
|
</span>
|
||||||
</div>
|
</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 -->
|
<!-- Play Description -->
|
||||||
<p
|
<p
|
||||||
class="text-sm text-gray-900 dark:text-gray-100 font-medium leading-relaxed"
|
class="text-sm text-gray-900 dark:text-gray-100 font-medium leading-relaxed"
|
||||||
@ -83,6 +88,20 @@
|
|||||||
{{ play.description }}
|
{{ play.description }}
|
||||||
</p>
|
</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) -->
|
<!-- Play Stats (Runs/Outs) -->
|
||||||
<div class="flex items-center gap-3 mt-3">
|
<div class="flex items-center gap-3 mt-3">
|
||||||
<!-- Runs Scored -->
|
<!-- Runs Scored -->
|
||||||
@ -138,8 +157,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PlayResult } from '~/types/game'
|
import type { PlayResult, RunnerAdvancement } from '~/types/game'
|
||||||
import { h } from 'vue'
|
import { h } from 'vue'
|
||||||
|
import { useGameStore } from '~/store/game'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
plays?: PlayResult[]
|
plays?: PlayResult[]
|
||||||
@ -159,9 +179,60 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
showFilters: true
|
showFilters: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Store for player name lookup
|
||||||
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const showAllPlays = ref(false)
|
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
|
// Computed
|
||||||
const displayedPlays = computed(() => {
|
const displayedPlays = computed(() => {
|
||||||
if (!props.plays || props.plays.length === 0) return []
|
if (!props.plays || props.plays.length === 0) return []
|
||||||
@ -175,9 +246,9 @@ const displayedPlays = computed(() => {
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const formatInning = (play: PlayResult): string => {
|
const formatInning = (play: PlayResult): string => {
|
||||||
// Extract inning from play (assuming it's in description or metadata)
|
if (!play.inning) return 'Inning ?'
|
||||||
// Fallback format for now
|
const half = play.half === 'top' ? 'Top' : play.half === 'bottom' ? 'Bot' : ''
|
||||||
return `Inning ${play.inning || '?'}`
|
return half ? `${half} ${play.inning}` : `Inning ${play.inning}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatOutcome = (outcome: string): string => {
|
const formatOutcome = (outcome: string): string => {
|
||||||
|
|||||||
@ -4,27 +4,39 @@
|
|||||||
* Protects routes that require authentication.
|
* Protects routes that require authentication.
|
||||||
* Redirects to login if user is not authenticated.
|
* 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'
|
import { useAuthStore } from '~/store/auth'
|
||||||
|
|
||||||
export default defineNuxtRouteMiddleware((to, from) => {
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
const authStore = useAuthStore()
|
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]', {
|
console.log('[Auth Middleware]', {
|
||||||
path: to.path,
|
path: to.path,
|
||||||
isAuthenticated: authStore.isAuthenticated,
|
isAuthenticated: isAuthed,
|
||||||
hasUser: !!authStore.currentUser,
|
hasUser: !!authStore.currentUser,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Allow access if authenticated
|
// Allow access if authenticated
|
||||||
if (authStore.isAuthenticated) {
|
if (isAuthed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login with return URL
|
// Redirect to login with return URL
|
||||||
console.log('[Auth Middleware] Redirecting to login')
|
console.log('[Auth Middleware] Not authenticated, redirecting to login')
|
||||||
return navigateTo({
|
return navigateTo({
|
||||||
path: '/auth/login',
|
path: '/auth/login',
|
||||||
query: { redirect: to.fullPath },
|
query: { redirect: to.fullPath },
|
||||||
|
|||||||
@ -38,14 +38,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Discord Login Button -->
|
<!-- Discord Login Button (anchor tag for proxy/iPad compatibility) -->
|
||||||
<button
|
<a
|
||||||
:disabled="isLoading"
|
:href="discordLoginUrl"
|
||||||
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"
|
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"
|
||||||
@click="handleDiscordLogin"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
v-if="!isLoading"
|
|
||||||
class="w-6 h-6"
|
class="w-6 h-6"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
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"
|
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>
|
</svg>
|
||||||
<div
|
<span>Continue with Discord</span>
|
||||||
v-if="isLoading"
|
</a>
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- Additional Info -->
|
<!-- Additional Info -->
|
||||||
<div class="mt-6 text-center text-sm text-gray-600">
|
<div class="mt-6 text-center text-sm text-gray-600">
|
||||||
@ -95,10 +89,16 @@ definePageMeta({
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
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
|
// Check if already authenticated
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (authStore.isAuthenticated) {
|
if (authStore.isAuthenticated) {
|
||||||
@ -107,20 +107,6 @@ onMounted(() => {
|
|||||||
router.push(redirect)
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -45,18 +45,38 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const config = useRuntimeConfig()
|
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<{
|
const response = await $fetch<{
|
||||||
user: DiscordUser
|
user: DiscordUser
|
||||||
teams: Team[]
|
teams: Team[]
|
||||||
}>(`${config.public.apiUrl}/api/auth/me`, {
|
}>(url, {
|
||||||
credentials: 'include', // Send cookies
|
credentials: 'include', // Send cookies (client-side)
|
||||||
|
headers, // Forward cookies (server-side)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('[Auth Store] checkAuth() success:', response.user?.username)
|
||||||
user.value = response.user
|
user.value = response.user
|
||||||
teams.value = response.teams
|
teams.value = response.teams
|
||||||
return true
|
return true
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Not authenticated or token expired
|
// Not authenticated or token expired
|
||||||
|
console.log('[Auth Store] checkAuth() failed:', err.message || err)
|
||||||
user.value = null
|
user.value = null
|
||||||
teams.value = []
|
teams.value = []
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -214,7 +214,8 @@ export type PlayOutcome =
|
|||||||
export interface RunnerAdvancement {
|
export interface RunnerAdvancement {
|
||||||
from: number // 0=batter, 1-3=bases
|
from: number // 0=batter, 1-3=bases
|
||||||
to: number // 1-4=bases (4=home/scored)
|
to: number // 1-4=bases (4=home/scored)
|
||||||
out?: boolean // Runner was out during advancement
|
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 {
|
export interface PlayResult {
|
||||||
// Play identification
|
// Play identification
|
||||||
play_number: number
|
play_number: number
|
||||||
|
inning?: number // Inning when play occurred
|
||||||
|
half?: InningHalf // 'top' or 'bottom'
|
||||||
outcome: PlayOutcome
|
outcome: PlayOutcome
|
||||||
|
|
||||||
// Play description
|
// Play description
|
||||||
@ -234,6 +237,9 @@ export interface PlayResult {
|
|||||||
outs_recorded: number
|
outs_recorded: number
|
||||||
runs_scored: number
|
runs_scored: number
|
||||||
|
|
||||||
|
// Player identification for display
|
||||||
|
batter_lineup_id?: number // Batter's lineup ID for name lookup
|
||||||
|
|
||||||
// Runner advancement
|
// Runner advancement
|
||||||
runners_advanced: RunnerAdvancement[]
|
runners_advanced: RunnerAdvancement[]
|
||||||
batter_result: number | null // Where batter ended up (1-4, null=out)
|
batter_result: number | null // Where batter ended up (1-4, null=out)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user