Enhance performance and error handling in the application

- Optimize release profile settings in Cargo.toml
- Update Dockerfile to include additional binary
- Modify compose.yml to adjust database URL for read/write mode
- Improve error handling in API calls within api.ts
- Refactor character properties in types.ts for clarity
- Update Card component to reflect new character properties
- Revise utility functions for better episode handling
This commit is contained in:
2025-07-20 15:15:30 +02:00
parent 27e9119123
commit c5de84c013
9 changed files with 95 additions and 75 deletions

View File

@@ -24,3 +24,10 @@ tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-log = "0.2.0" tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt"] }
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true

View File

@@ -20,6 +20,7 @@ WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates openssl sqlite3 && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y ca-certificates openssl sqlite3 && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/data RUN mkdir -p /app/data
COPY --from=backend-builder /app/target/release/rick-and-morty . COPY --from=backend-builder /app/target/release/rick-and-morty .
COPY --from=backend-builder /app/target/release/fetch_characters .
COPY --from=backend-builder /app/migrations ./migrations COPY --from=backend-builder /app/migrations ./migrations
COPY --from=frontend-builder /app/dist ./frontend/dist COPY --from=frontend-builder /app/dist ./frontend/dist
EXPOSE 8000 EXPOSE 8000

View File

@@ -3,7 +3,7 @@ services:
build: . build: .
container_name: rick_and_morty_app container_name: rick_and_morty_app
environment: environment:
DATABASE_URL: "sqlite:///app/data/rick_and_morty.db" DATABASE_URL: "sqlite:///app/data/rick_and_morty.db?mode=rwc"
BIND_ADDR: 0.0.0.0:8000 BIND_ADDR: 0.0.0.0:8000
ports: ports:
- "8000:8000" - "8000:8000"

Binary file not shown.

View File

@@ -4,21 +4,22 @@ import { getCharacters, rateCharacters } from "./api";
import type { Character } from "./types"; import type { Character } from "./types";
import { Card } from "./components/card"; import { Card } from "./components/card";
import { Table } from "./components/table"; import { Table } from "./components/table";
import { getId } from "./utils";
function getRandomIndex(length: number) { function getRandomIndex(length: number) {
return Math.floor(Math.random() * length); return Math.floor(Math.random() * length);
} }
function getRandomPair(characters: Character[], lastPair: [number, number]) { // Optionally: Use a lastPairRef to avoid same characters twice in a row
if (characters.length < 2) return [null, null]; function getRandomPair(
characters: Character[],
lastPair: [number, number] | null
): [Character, Character] {
if (characters.length < 2) return [null, null] as never;
let firstIndex = getRandomIndex(characters.length); let firstIndex = getRandomIndex(characters.length);
let secondIndex = getRandomIndex(characters.length); let secondIndex = getRandomIndex(characters.length);
while ( while (
firstIndex === lastPair[0] || (lastPair && firstIndex === lastPair[0] && secondIndex === lastPair[1]) ||
secondIndex === lastPair[1] ||
firstIndex === secondIndex firstIndex === secondIndex
) { ) {
firstIndex = getRandomIndex(characters.length); firstIndex = getRandomIndex(characters.length);
@@ -57,8 +58,8 @@ const App: React.FC = () => {
if (voting || !rivals[winnerIdx] || !rivals[loserIdx]) return; if (voting || !rivals[winnerIdx] || !rivals[loserIdx]) return;
setVoting(true); setVoting(true);
setVotedLeft(winnerIdx === 0); setVotedLeft(winnerIdx === 0);
const winnerId = getId(rivals[winnerIdx]); const winnerId = rivals[winnerIdx]!.id;
const loserId = getId(rivals[loserIdx]); const loserId = rivals[loserIdx]!.id;
if (!winnerId || !loserId) return; if (!winnerId || !loserId) return;
await rateCharacters(winnerId, loserId); await rateCharacters(winnerId, loserId);
const chars = await getCharacters(); const chars = await getCharacters();

View File

@@ -2,13 +2,19 @@ import type { Character } from './types';
export const getCharacters = async (): Promise<Character[]> => { export const getCharacters = async (): Promise<Character[]> => {
const res = await fetch('/characters'); const res = await fetch('/characters');
if (!res.ok) {
throw new Error('Failed to fetch characters');
}
return res.json(); return res.json();
}; };
export const rateCharacters = async (winnerId: string, loserId: string) => { export const rateCharacters = async (winnerId: number, loserId: number) => {
await fetch('/rate', { const res = await fetch('/rate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ winner_id: winnerId, loser_id: loserId }), body: JSON.stringify({ winner_id: winnerId.toString(), loser_id: loserId.toString() }),
}); });
if (!res.ok) {
throw new Error('Failed to rate characters');
}
}; };

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from "react";
import type { Character } from '../types'; import type { Character } from "../types";
interface CardProps { interface CardProps {
data: Character; data: Character;
@@ -16,9 +16,11 @@ export const Card: React.FC<CardProps> = ({
onClick, onClick,
}) => ( }) => (
<div <div
className={`card bg-gray-300 flex flex-col items-center shadow-lg rounded p-1 transform transition ease-in-out ${isClicked || skipped ? 'flip-card' : ''} md:hover:scale-105`} className={`card bg-gray-300 flex flex-col items-center shadow-lg rounded p-1 transform transition ease-in-out ${
isClicked || skipped ? "flip-card" : ""
} md:hover:scale-105`}
onClick={onClick} onClick={onClick}
style={{ cursor: 'pointer' }} style={{ cursor: "pointer" }}
> >
<div className="card-inner"> <div className="card-inner">
{/* FRONT */} {/* FRONT */}
@@ -43,11 +45,11 @@ export const Card: React.FC<CardProps> = ({
</tr> </tr>
<tr> <tr>
<th>Origin</th> <th>Origin</th>
<td>{data.origin.name}</td> <td>{data.origin_name}</td>
</tr> </tr>
<tr> <tr>
<th>Last location</th> <th>Last location</th>
<td>{data.location.name}</td> <td>{data.location_name}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -4,17 +4,19 @@ export interface OriginOrLocation {
} }
export interface Character { export interface Character {
_id: string | { $oid: string }; id: number;
rmid: number; rmid: number;
name: string; name: string;
status: string; status: string;
species: string; species: string;
type: string; type: string;
gender: string; gender: string;
origin: OriginOrLocation; origin_name: string;
location: OriginOrLocation; origin_url: string;
location_name: string;
location_url: string;
image: string; image: string;
episode: string[]; episode: string; // This will be a stringified JSON array!
url: string; url: string;
created: string; created: string;
elo_rating: number; elo_rating: number;

View File

@@ -1,9 +1,10 @@
import type { Character } from "./types"; import type { Character } from "./types";
export function getId(character: Character | null) { export function getEpisodes(character: Character): string[] {
if (!character) return undefined; if (Array.isArray(character.episode)) return character.episode;
const id = character._id as string | { $oid: string } | undefined; try {
if (typeof id === 'string') return id; return JSON.parse(character.episode);
if (id && typeof id.$oid === 'string') return id.$oid; } catch {
return undefined; return [];
}
} }