300 lines
8.8 KiB
Vue
300 lines
8.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import PuzzleCard from './components/PuzzleCard.vue'
|
|
import SubmissionForm from './components/SubmissionForm.vue'
|
|
import { puzzleHelpers, submissionHelpers, errorHelpers } from './services/apiService'
|
|
import type { SteamCollection, SteamCollectionItem, Submission, PuzzleResponse } from './types'
|
|
|
|
// API data
|
|
const collections = ref<SteamCollection[]>([])
|
|
const puzzles = ref<SteamCollectionItem[]>([])
|
|
const submissions = ref<Submission[]>([])
|
|
const isLoading = ref(true)
|
|
const showSubmissionModal = ref(false)
|
|
const error = ref<string>('')
|
|
|
|
// Mock data for development
|
|
const mockCollections: SteamCollection[] = [
|
|
{
|
|
id: 1,
|
|
steam_id: '3479142989',
|
|
title: 'PolyLAN 41',
|
|
description: 'Puzzle for PolyLAN 41 fil rouge',
|
|
author_name: 'Flame Legrems',
|
|
total_items: 10,
|
|
unique_visitors: 31,
|
|
current_favorites: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
}
|
|
]
|
|
|
|
const mockPuzzles: SteamCollectionItem[] = [
|
|
{
|
|
id: 1,
|
|
steam_item_id: '3479143948',
|
|
title: 'P41-FLOC',
|
|
author_name: 'Flame Legrems',
|
|
description: 'A challenging puzzle involving complex molecular arrangements',
|
|
tags: ['puzzle', 'chemistry', 'advanced'],
|
|
order_index: 0,
|
|
collection: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
},
|
|
{
|
|
id: 2,
|
|
steam_item_id: '3479143084',
|
|
title: 'P41-40',
|
|
author_name: 'Flame Legrems',
|
|
description: 'Test your optimization skills with this intricate design challenge',
|
|
tags: ['optimization', 'design'],
|
|
order_index: 1,
|
|
collection: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
},
|
|
{
|
|
id: 3,
|
|
steam_item_id: '3479143304',
|
|
title: 'P41-39',
|
|
author_name: 'Flame Legrems',
|
|
description: 'A puzzle focusing on efficient resource management',
|
|
tags: ['efficiency', 'resources'],
|
|
order_index: 2,
|
|
collection: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
},
|
|
{
|
|
id: 4,
|
|
steam_item_id: '3479143433',
|
|
title: 'P41-38',
|
|
author_name: 'Flame Legrems',
|
|
description: 'Master the art of precise timing in this temporal challenge',
|
|
tags: ['timing', 'precision'],
|
|
order_index: 3,
|
|
collection: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
},
|
|
{
|
|
id: 5,
|
|
steam_item_id: '3479143537',
|
|
title: 'P41-37',
|
|
author_name: 'Flame Legrems',
|
|
description: 'Explore innovative solutions in this creative puzzle',
|
|
tags: ['creative', 'innovation'],
|
|
order_index: 4,
|
|
collection: 1,
|
|
created_at: '2025-05-29T11:19:24Z',
|
|
updated_at: '2025-05-30T22:15:09Z'
|
|
}
|
|
]
|
|
|
|
// Computed property to get responses grouped by puzzle
|
|
const responsesByPuzzle = computed(() => {
|
|
const grouped: Record<number, PuzzleResponse[]> = {}
|
|
submissions.value.forEach(submission => {
|
|
submission.responses.forEach(response => {
|
|
if (!grouped[response.puzzle_id]) {
|
|
grouped[response.puzzle_id] = []
|
|
}
|
|
grouped[response.puzzle_id].push(response)
|
|
})
|
|
})
|
|
return grouped
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
isLoading.value = true
|
|
error.value = ''
|
|
|
|
// Load puzzles from API
|
|
const loadedPuzzles = await puzzleHelpers.loadPuzzles()
|
|
puzzles.value = loadedPuzzles
|
|
|
|
// Create mock collection from loaded puzzles for display
|
|
if (loadedPuzzles.length > 0) {
|
|
collections.value = [{
|
|
id: 1,
|
|
steam_id: '3479142989',
|
|
title: 'PolyLAN 41',
|
|
description: 'Puzzle collection for PolyLAN 41 fil rouge',
|
|
author_name: 'Flame Legrems',
|
|
total_items: loadedPuzzles.length,
|
|
unique_visitors: 31,
|
|
current_favorites: 1,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
}]
|
|
}
|
|
|
|
// Load existing submissions
|
|
const loadedSubmissions = await submissionHelpers.loadSubmissions()
|
|
submissions.value = loadedSubmissions
|
|
|
|
} catch (err) {
|
|
error.value = errorHelpers.getErrorMessage(err)
|
|
console.error('Failed to load data:', err)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
})
|
|
|
|
const handleSubmission = async (submissionData: {
|
|
files: any[],
|
|
notes?: string
|
|
}) => {
|
|
try {
|
|
isLoading.value = true
|
|
error.value = ''
|
|
|
|
// Create submission via API
|
|
const response = await submissionHelpers.createFromFiles(
|
|
submissionData.files,
|
|
puzzles.value,
|
|
submissionData.notes
|
|
)
|
|
|
|
if (response.error) {
|
|
error.value = response.error
|
|
alert(`Submission failed: ${response.error}`)
|
|
return
|
|
}
|
|
|
|
if (response.data) {
|
|
// Add to local submissions list
|
|
submissions.value.unshift(response.data)
|
|
|
|
// Show success message
|
|
const puzzleNames = response.data.responses.map(r => r.puzzle_name).join(', ')
|
|
alert(`Solutions submitted successfully for puzzles: ${puzzleNames}`)
|
|
|
|
// Close modal
|
|
showSubmissionModal.value = false
|
|
}
|
|
|
|
} catch (err) {
|
|
const errorMessage = errorHelpers.getErrorMessage(err)
|
|
error.value = errorMessage
|
|
alert(`Submission failed: ${errorMessage}`)
|
|
console.error('Submission error:', err)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const openSubmissionModal = () => {
|
|
showSubmissionModal.value = true
|
|
}
|
|
|
|
const closeSubmissionModal = () => {
|
|
showSubmissionModal.value = false
|
|
}
|
|
|
|
// Function to match puzzle name from OCR to actual puzzle
|
|
const findPuzzleByName = (ocrPuzzleName: string): SteamCollectionItem | null => {
|
|
return puzzleHelpers.findPuzzleByName(puzzles.value, ocrPuzzleName)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="min-h-screen bg-base-200">
|
|
<!-- Header -->
|
|
<div class="navbar bg-base-100 shadow-lg">
|
|
<div class="container mx-auto">
|
|
<div class="flex-1">
|
|
<h1 class="text-xl font-bold">Opus Magnum Puzzle Submitter</h1>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="container mx-auto px-4 py-8">
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex justify-center items-center min-h-[400px]">
|
|
<div class="text-center">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
<p class="mt-4 text-base-content/70">Loading puzzles...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="alert alert-error max-w-2xl mx-auto">
|
|
<i class="mdi mdi-alert-circle text-xl"></i>
|
|
<div>
|
|
<h3 class="font-bold">Error Loading Data</h3>
|
|
<div class="text-sm">{{ error }}</div>
|
|
</div>
|
|
<button @click="window.location.reload()" class="btn btn-sm btn-outline">
|
|
<i class="mdi mdi-refresh mr-1"></i>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div v-else class="space-y-8">
|
|
<!-- Collection Info -->
|
|
<div v-if="collections.length > 0" class="mb-8">
|
|
<div class="card bg-base-100 shadow-lg">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl">{{ collections[0].title }}</h2>
|
|
<p class="text-base-content/70">{{ collections[0].description }}</p>
|
|
<div class="flex flex-wrap gap-4 mt-4">
|
|
<button
|
|
@click="openSubmissionModal"
|
|
class="btn btn-primary"
|
|
>
|
|
<i class="mdi mdi-plus mr-2"></i>
|
|
Submit Solution
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Puzzles Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<PuzzleCard
|
|
v-for="puzzle in puzzles"
|
|
:key="puzzle.id"
|
|
:puzzle="puzzle"
|
|
:responses="responsesByPuzzle[puzzle.id] || []"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-if="puzzles.length === 0" class="text-center py-12">
|
|
<div class="text-6xl mb-4">🧩</div>
|
|
<h3 class="text-xl font-bold mb-2">No Puzzles Available</h3>
|
|
<p class="text-base-content/70">Check back later for new puzzle collections!</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submission Modal -->
|
|
<div v-if="showSubmissionModal" class="modal modal-open">
|
|
<div class="modal-box max-w-4xl">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="font-bold text-lg">Submit Solution</h3>
|
|
<button
|
|
@click="closeSubmissionModal"
|
|
class="btn btn-sm btn-circle btn-ghost"
|
|
>
|
|
<i class="mdi mdi-close"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<SubmissionForm
|
|
:puzzles="puzzles"
|
|
:find-puzzle-by-name="findPuzzleByName"
|
|
@submit="handleSubmission"
|
|
/>
|
|
</div>
|
|
<div class="modal-backdrop" @click="closeSubmissionModal"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|