ninja fix

This commit is contained in:
2025-01-28 22:26:18 +01:00
parent 6f78073431
commit 7e5baa96e8
8 changed files with 9 additions and 6 deletions
+6 -2
View File
@@ -5,6 +5,7 @@ import (
"encoding/gob"
"gopher-toolbox/db"
"log/slog"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
@@ -23,9 +24,12 @@ func init() {
//go:embed database/migrations
var database embed.FS
//go:embed templates
var templates embed.FS
func main() {
engine := html.New("./views", ".html")
engine.Reload(true)
engine := html.NewFileSystem(http.FS(templates), ".gotmpl")
engine.Directory = "templates"
app := app.NewExtendedApp(appName, version, ".env")
app.Migrate(database)
+22
View File
@@ -0,0 +1,22 @@
<div class="flex flex-col justify-center min-h-screen">
<form class="max-w-md mx-auto w-full" action="/tvshow" method="get">
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Buscar</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-search">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
</div>
<input type="search" id="default-search"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="IMDb ID" required
name="ttid"
/>
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Search</button>
</div>
</form>
</div>
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ .Title }}</title>
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
</head>
<body class="min-h-screen bg-slate-50">
{{ embed }}
</body>
</html>
+51
View File
@@ -0,0 +1,51 @@
<footer class="bg-slate-200" aria-label="Site Footer">
<ul class="mt-12 flex justify-center gap-6 p-12">
<li>
<a
class="text-slate-700 transition hover:text-slate-700/75"
href="https://www.instagram.com/modelektor/"
rel="noreferrer"
target="_blank"
>
<span class="sr-only">Instagram</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-instagram"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/></svg>
</a>
</li>
<li>
<a
class="text-slate-700 transition hover:text-slate-700/75"
href="https://github.com/zepyrshut"
rel="noreferrer"
target="_blank"
>
<span class="sr-only">GitHub</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
</a>
</li>
<li>
<a
class="text-slate-700 transition hover:text-slate-700/75"
href="https://pedroperez.dev/"
rel="noreferrer"
target="_blank"
>
<span class="sr-only">Website</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
</a>
</li>
<li>
<a
class="text-slate-700 transition hover:text-slate-700/75"
href="https://github.com/zepyrshut/rating-orama"
rel="noreferrer"
target="_blank"
>
<span class="sr-only">Source</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>
</a>
</li>
</ul>
</footer>
+334
View File
@@ -0,0 +1,334 @@
<div class="flex flex-col items-center gap-4 mb-8 p-2">
<!-- Card header -->
<div class="flex flex-col items-center mt-4 bg-white p-4 rounded-lg shadow-md w-1/2">
<div class="flex flex-col items-center space-y-4 w-full">
<!-- Title section -->
<div class="text-center">
<h1 class="text-3xl font-bold text-gray-800">{{ .tvshow.Name }}</h1>
<div class="mt-1 flex items-center justify-center space-x-2">
<a href="https://www.imdb.com/title/{{ .tvshow.TtImdb }}" target="_blank"
class="text-amber-500 hover:text-amber-600 flex items-center space-x-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span class="font-semibold text-sm">{{ .tvshow.TtImdb }}</span>
</a>
</div>
</div>
<!-- Ratings section -->
<div class="grid grid-cols-2 gap-4 w-full">
<div class="bg-gray-50 rounded-lg p-2 text-center">
<span class="text-xs text-gray-500 uppercase">Average</span>
<div class="text-2xl font-bold text-gray-800">{{ printf "%.2f" .avg_rating_show }}</div>
</div>
<div class="bg-gray-50 rounded-lg p-2 text-center">
<span class="text-xs text-gray-500 uppercase">Median</span>
<div class="text-2xl font-bold text-gray-800">{{ printf "%.2f" .median_rating_show }}</div>
</div>
</div>
<!-- Stats section -->
<div class="flex justify-around w-full px-3 py-2 bg-gray-50 rounded-lg">
<div class="text-center">
<span class="text-xs text-gray-500">Vote count</span>
<div class="text-lg font-bold text-gray-800">{{ .total_vote_count }}</div>
</div>
<div class="text-center">
<span class="text-xs text-gray-500">Times searched</span>
<div class="text-lg font-bold text-gray-800">{{ .tvshow.Popularity }}</div>
</div>
</div>
</div>
</div>
<div class="w-4/5 mx-6">
<h2 class="mb-6 text-center text-2xl font-bold">Seasons overall</h2>
<canvas id="seasons"></canvas>
</div>
<div class="w-4/5">
<h2 class="mb-6 text-center text-2xl font-bold">Season <span id="seasonNumber">1</span></h2>
<div id="seasonButtons" class="mb-4 flex flex-wrap items-center justify-center gap-2"></div>
<canvas id="episodes"></canvas>
</div>
</div>
{{ template "partials/footer" . }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"
integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="module">
const episodes = JSON.parse("{{ .episodes }}")
function groupEpisodesBySeason(episodes) {
const seasons = {};
episodes.forEach(episode => {
if (!seasons[episode.season]) {
seasons[episode.season] = {
number: episode.season,
episodes: [],
avg_rating: 0,
median_rating: 0,
votes: 0
};
}
seasons[episode.season].episodes.push({
number: episode.episode,
title: episode.name,
avg_rating: episode.avg_rating,
votes: episode.vote_count,
aired: episode.released
});
});
Object.values(seasons).forEach(season => {
const ratings = season.episodes.map(ep => ep.avg_rating);
season.avg_rating = ratings.reduce((a, b) => a + b, 0) / ratings.length;
season.median_rating = ratings.sort((a, b) => a - b)[Math.floor(ratings.length / 2)];
season.votes = season.episodes.reduce((sum, ep) => sum + ep.votes, 0);
});
return {
seasons: Object.values(seasons).sort((a, b) => a.number - b.number)
};
}
function createSeasonButtons() {
const maxSeason = Math.max(...episodes.map(ep => ep.season));
const buttonContainer = document.getElementById('seasonButtons');
const seasonNumberSpan = document.getElementById('seasonNumber');
for (let i = 1; i <= maxSeason; i++) {
const button = document.createElement('button');
button.textContent = `S${i}`;
button.className = 'rounded-md border-2 border-black px-4 py-2 font-semibold transition-colors duration-200 hover:bg-black hover:text-white';
if (i === 1) button.classList.add('bg-black', 'text-white');
button.addEventListener('click', (e) => {
e.preventDefault();
buttonContainer.querySelectorAll('button').forEach(btn => {
btn.classList.remove('bg-black', 'text-white');
btn.classList.add('text-black');
});
button.classList.remove('text-black');
button.classList.add('bg-black', 'text-white');
seasonNumberSpan.textContent = i;
const formattedData = groupEpisodesBySeason(episodes);
loadSpecificSeason(formattedData, i);
});
buttonContainer.appendChild(button);
}
}
document.addEventListener("DOMContentLoaded", function () {
const formattedData = groupEpisodesBySeason(episodes);
initCharts(formattedData);
createSeasonButtons();
});
function loadSeason(seasonNumber) {
const formattedData = groupEpisodesBySeason(episodes);
loadSpecificSeason(formattedData, seasonNumber);
}
function initCharts(tvShowParsed) {
new EpisodeChart(tvShowParsed, "episodes", 0);
new SeasonsChart(tvShowParsed, "seasons");
}
function loadSpecificSeason(tvShowParsed, seasonId) {
// Guardamos la posición actual del scroll
const currentScroll = window.scrollY;
new EpisodeChart(tvShowParsed, "episodes", seasonId - 1);
// Restauramos la posición del scroll
window.scrollTo(0, currentScroll);
}
let chartInstance;
class SeasonsChart {
constructor(tvShowParsed, canvasId) {
this.tvShowParsed = tvShowParsed;
this.canvasId = canvasId;
this.createChart();
}
createChart() {
const seasons = this.tvShowParsed.seasons;
const labels = seasons.map((season) => `Season ${season.number}`);
const averageRating = seasons.map((season) => season.avg_rating);
const medianRating = seasons.map((season) => season.median_rating);
const votes = seasons.map((season) => season.votes);
const title = "Seasons"
const ctx = document.getElementById(this.canvasId);
new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "Average rating",
data: averageRating,
backgroundColor: "rgba(75, 192, 192, 0.5)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 1,
Range: 10,
yAxisID: "y-axis-ratings",
},
{
label: "Median rating",
data: medianRating,
backgroundColor: "rgba(255, 206, 86, 0.5)",
borderColor: "rgba(255, 206, 86, 1)",
borderWidth: 1,
Range: 10,
yAxisID: "y-axis-ratings",
},
{
label: "Votes",
data: votes,
type: "line",
tension: 0.4,
fill: false,
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 2,
yAxisID: "y-axis-votes",
},
],
},
options: {
animation: {
duration: 0,
},
y: {
stacked: true,
},
scales: {
"y-axis-ratings": {
min: 0,
max: 10,
type: "linear",
display: true,
position: "left",
beginAtZero: true,
},
"y-axis-votes": {
type: "linear",
display: true,
position: "right",
beginAtZero: true,
},
},
},
plugins: {
title: {
display: true,
text: title,
},
}
});
}
}
class EpisodeChart {
constructor(tvShowParsed, canvasId, seasonId) {
this.tvShowParsed = tvShowParsed;
this.canvasId = canvasId;
this.seasonId = seasonId;
this.createChart();
}
createChart() {
if (chartInstance) {
chartInstance.destroy();
}
const episodes = this.tvShowParsed.seasons[this.seasonId].episodes;
const labels = episodes.map((episode) => `Ep. ${episode.number}`);
const ratings = episodes.map((episode) => episode.avg_rating);
const votes = episodes.map((episode) => episode.votes);
const title = `Episodes of season ${this.seasonId + 1}`
const ctx = document.getElementById(this.canvasId);
chartInstance = new Chart(ctx, {
type: "bar",
data: {
labels: labels,
datasets: [
{
label: "Average rating",
data: ratings,
backgroundColor: "rgba(75, 192, 192, 0.5)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 1,
Range: 10,
yAxisID: "y-axis-ratings",
},
{
label: "Votes",
data: votes,
type: "line",
tension: 0.4,
fill: false,
borderColor: "rgba(255, 99, 132, 1)",
borderWidth: 2,
yAxisID: "y-axis-votes",
},
],
},
options: {
animation: {
duration: 0,
},
scales: {
"y-axis-ratings": {
min: 0,
max: 10,
type: "linear",
display: true,
position: "left",
beginAtZero: true,
},
"y-axis-votes": {
type: "linear",
display: true,
position: "right",
beginAtZero: true,
},
},
plugins: {
tooltip: {
callbacks: {
title: function (context) {
const index = context[0].dataIndex;
const episode = episodes[index];
return `${episode.title} (${episode.aired.split("T")[0]})`;
},
},
},
},
},
});
}
}
</script>