ninja fix
This commit is contained in:
+6
-2
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user