This commit is contained in:
+1
-1
@@ -72,7 +72,7 @@
|
|||||||
"hero-subtitle": "Make smarter bets with our advanced AI prediction engine. Analyze matches, discover value bets, and build winning coupons.",
|
"hero-subtitle": "Make smarter bets with our advanced AI prediction engine. Analyze matches, discover value bets, and build winning coupons.",
|
||||||
"get-started": "Get Started",
|
"get-started": "Get Started",
|
||||||
"learn-more": "Learn More",
|
"learn-more": "Learn More",
|
||||||
"features-title": "Why Choose Suggest Bet?",
|
"features-title": "Why Choose Iddaai?",
|
||||||
"feature-ai": "AI Predictions",
|
"feature-ai": "AI Predictions",
|
||||||
"feature-ai-desc": "Powered by V20 ensemble model with 95%+ data quality scoring.",
|
"feature-ai-desc": "Powered by V20 ensemble model with 95%+ data quality scoring.",
|
||||||
"feature-value": "Value Bets",
|
"feature-value": "Value Bets",
|
||||||
|
|||||||
+75
-15
@@ -5,8 +5,8 @@
|
|||||||
"intelligent-transportation-systems": "Akıllı Ulaşım Sistemleri",
|
"intelligent-transportation-systems": "Akıllı Ulaşım Sistemleri",
|
||||||
"artificial-intelligence": "Yapay Zeka",
|
"artificial-intelligence": "Yapay Zeka",
|
||||||
"error": {
|
"error": {
|
||||||
"not-found": "Aradığınız sayfa bulunamadı.",
|
|
||||||
"404": "404",
|
"404": "404",
|
||||||
|
"not-found": "Aradığınız sayfa bulunamadı.",
|
||||||
"back-to-home": "Ana sayfaya dön",
|
"back-to-home": "Ana sayfaya dön",
|
||||||
"generic": "Beklenmeyen bir hata oluştu.",
|
"generic": "Beklenmeyen bir hata oluştu.",
|
||||||
"network": "Ağ hatası. Lütfen bağlantınızı kontrol edin.",
|
"network": "Ağ hatası. Lütfen bağlantınızı kontrol edin.",
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
"low": "Düşük",
|
"low": "Düşük",
|
||||||
"medium": "Orta",
|
"medium": "Orta",
|
||||||
"high": "Yüksek",
|
"high": "Yüksek",
|
||||||
|
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "Anasayfa",
|
"home": "Anasayfa",
|
||||||
"dashboard": "Kontrol Paneli",
|
"dashboard": "Kontrol Paneli",
|
||||||
@@ -66,13 +65,12 @@
|
|||||||
"coupons": "Kuponlar",
|
"coupons": "Kuponlar",
|
||||||
"tools": "Araçlar"
|
"tools": "Araçlar"
|
||||||
},
|
},
|
||||||
|
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero-title": "Yapay Zeka Destekli Bahis Tahminleri",
|
"hero-title": "Yapay Zeka Destekli Bahis Tahminleri",
|
||||||
"hero-subtitle": "Gelişmiş yapay zeka tahmin motorumuz ile daha akıllı bahisler yapın. Maçları analiz edin, değerli bahisleri keşfedin ve kazanan kuponlar oluşturun.",
|
"hero-subtitle": "Gelişmiş yapay zeka tahmin motorumuz ile daha akıllı bahisler yapın. Maçları analiz edin, değerli bahisleri keşfedin ve kazanan kuponlar oluşturun.",
|
||||||
"get-started": "Başla",
|
"get-started": "Başla",
|
||||||
"learn-more": "Daha Fazla",
|
"learn-more": "Daha Fazla",
|
||||||
"features-title": "Neden Suggest Bet?",
|
"features-title": "Neden Iddaai?",
|
||||||
"feature-ai": "Yapay Zeka Tahminleri",
|
"feature-ai": "Yapay Zeka Tahminleri",
|
||||||
"feature-ai-desc": "%95+ veri kalitesi puanlama ile V20 ensemble modeli tarafından desteklenmektedir.",
|
"feature-ai-desc": "%95+ veri kalitesi puanlama ile V20 ensemble modeli tarafından desteklenmektedir.",
|
||||||
"feature-value": "Değerli Bahisler",
|
"feature-value": "Değerli Bahisler",
|
||||||
@@ -86,7 +84,6 @@
|
|||||||
"stats-users": "Aktif Kullanıcı",
|
"stats-users": "Aktif Kullanıcı",
|
||||||
"stats-matches": "Analiz Edilen Maç"
|
"stats-matches": "Analiz Edilen Maç"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Kontrol Paneli",
|
"title": "Kontrol Paneli",
|
||||||
"welcome": "Tekrar hoş geldiniz",
|
"welcome": "Tekrar hoş geldiniz",
|
||||||
@@ -99,7 +96,6 @@
|
|||||||
"no-matches": "Bugün maç bulunmuyor.",
|
"no-matches": "Bugün maç bulunmuyor.",
|
||||||
"no-predictions": "Tahmin bulunmuyor."
|
"no-predictions": "Tahmin bulunmuyor."
|
||||||
},
|
},
|
||||||
|
|
||||||
"matches": {
|
"matches": {
|
||||||
"title": "Maçlar",
|
"title": "Maçlar",
|
||||||
"filter-sport": "Spor",
|
"filter-sport": "Spor",
|
||||||
@@ -123,7 +119,6 @@
|
|||||||
"away-team": "Deplasman",
|
"away-team": "Deplasman",
|
||||||
"vs": "vs"
|
"vs": "vs"
|
||||||
},
|
},
|
||||||
|
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"title": "Tahminler",
|
"title": "Tahminler",
|
||||||
"upcoming": "Yaklaşan",
|
"upcoming": "Yaklaşan",
|
||||||
@@ -251,7 +246,6 @@
|
|||||||
"recommended-stake-inline": "Önerilen miktar"
|
"recommended-stake-inline": "Önerilen miktar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"coupons": {
|
"coupons": {
|
||||||
"title": "Kupon Oluşturucu",
|
"title": "Kupon Oluşturucu",
|
||||||
"builder-title": "Kupon Oluşturucu",
|
"builder-title": "Kupon Oluşturucu",
|
||||||
@@ -388,15 +382,14 @@
|
|||||||
"win-rate": "Kazanma Oranı",
|
"win-rate": "Kazanma Oranı",
|
||||||
"total-profit": "Toplam Kâr"
|
"total-profit": "Toplam Kâr"
|
||||||
},
|
},
|
||||||
|
|
||||||
"leagues": {
|
"leagues": {
|
||||||
"title": "Ligler & Takımlar",
|
"title": "Ligler & Takımlar",
|
||||||
"countries": "Ülkeler",
|
"countries": "Ülkeler",
|
||||||
"leagues": "Ligler",
|
"leagues": "Ligler",
|
||||||
"countries-leagues": "Ülkeler & Ligler",
|
"countries-leagues": "Ülkeler & Ligler",
|
||||||
"search-at-least-2": "Takım aramak için en az 2 karakter yazın."
|
"search-at-least-2": "Takım aramak için en az 2 karakter yazın.",
|
||||||
|
"all": "Tümü"
|
||||||
},
|
},
|
||||||
|
|
||||||
"h2h": {
|
"h2h": {
|
||||||
"title": "Karşılıklı Karşılaşma",
|
"title": "Karşılıklı Karşılaşma",
|
||||||
"team-1": "Takım 1",
|
"team-1": "Takım 1",
|
||||||
@@ -406,7 +399,6 @@
|
|||||||
"draws": "Beraberlikler",
|
"draws": "Beraberlikler",
|
||||||
"no-matches-found": "Bu takımlar arasında karşılıklı maç bulunamadı."
|
"no-matches-found": "Bu takımlar arasında karşılıklı maç bulunamadı."
|
||||||
},
|
},
|
||||||
|
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"title": "Çoklu Maç Analizi",
|
"title": "Çoklu Maç Analizi",
|
||||||
"select-matches": "Maç Seç",
|
"select-matches": "Maç Seç",
|
||||||
@@ -418,7 +410,6 @@
|
|||||||
"matches-analyzed": "maç analiz edildi",
|
"matches-analyzed": "maç analiz edildi",
|
||||||
"no-history": "Henüz analiz geçmişi yok."
|
"no-history": "Henüz analiz geçmişi yok."
|
||||||
},
|
},
|
||||||
|
|
||||||
"spor-toto": {
|
"spor-toto": {
|
||||||
"title": "Spor Toto",
|
"title": "Spor Toto",
|
||||||
"sync-bulletins": "Bültenleri Senkronize Et",
|
"sync-bulletins": "Bültenleri Senkronize Et",
|
||||||
@@ -446,7 +437,6 @@
|
|||||||
"rollover-stats": "Devir İstatistikleri",
|
"rollover-stats": "Devir İstatistikleri",
|
||||||
"prediction-generated": "Tahmin başarıyla oluşturuldu!"
|
"prediction-generated": "Tahmin başarıyla oluşturuldu!"
|
||||||
},
|
},
|
||||||
|
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Yönetim Paneli",
|
"title": "Yönetim Paneli",
|
||||||
"subtitle": "Kullanıcıları yönetin, tahminleri takip edin ve sistemi izleyin.",
|
"subtitle": "Kullanıcıları yönetin, tahminleri takip edin ve sistemi izleyin.",
|
||||||
@@ -476,7 +466,6 @@
|
|||||||
"user-status": "Durum",
|
"user-status": "Durum",
|
||||||
"no-users": "Kullanıcı bulunamadı."
|
"no-users": "Kullanıcı bulunamadı."
|
||||||
},
|
},
|
||||||
|
|
||||||
"common": {
|
"common": {
|
||||||
"loading": "Yükleniyor...",
|
"loading": "Yükleniyor...",
|
||||||
"save": "Kaydet",
|
"save": "Kaydet",
|
||||||
@@ -505,5 +494,76 @@
|
|||||||
"items-per-page": "Sayfa başına öğe",
|
"items-per-page": "Sayfa başına öğe",
|
||||||
"showing": "Gösterilen",
|
"showing": "Gösterilen",
|
||||||
"results": "sonuç"
|
"results": "sonuç"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"global": {
|
||||||
|
"title": "iddaai.com | Yapay Zeka İddaa Tahminleri",
|
||||||
|
"description": "iddaai.com yapay zeka destekli iddaa tahminleri, detaylı maç analizleri ve veriye dayalı kupon oluşturma hizmeti sunar.",
|
||||||
|
"keywords": "iddaa, iddaa tahminleri, yapay zeka iddaa, maç analizi, banko kuponlar, futbol istatistikleri"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"title": "Ana Sayfa",
|
||||||
|
"description": "Yapay zeka destekli iddaa tahminleri. Maçları analiz edin, değerli bahisleri keşfedin ve kazandıran kuponlar oluşturun."
|
||||||
|
},
|
||||||
|
"h2h": {
|
||||||
|
"title": "Takım Karşılaştırma (H2H)",
|
||||||
|
"description": "Futbol takımlarını birebir karşılaştırın. Derinlemesine istatistikler, geçmiş maçlar ve tahminler."
|
||||||
|
},
|
||||||
|
"analysis": {
|
||||||
|
"title": "Çoklu Maç Analizi",
|
||||||
|
"description": "Aynı anda birden fazla maçı analiz edin. Detaylı istatistikler ve yapay zeka ile stratejiler geliştirin."
|
||||||
|
},
|
||||||
|
"leagues": {
|
||||||
|
"title": "Ligler ve Takımlar",
|
||||||
|
"description": "Dünya çapındaki futbol ve basketbol liglerini, ülkeleri ve takım istatistiklerini inceleyin."
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Yönetici Paneli",
|
||||||
|
"description": "Kullanıcıları ve sistem ayarlarını yönetmek için yönetici paneli."
|
||||||
|
},
|
||||||
|
"matches": {
|
||||||
|
"title": "Maçlar ve Fikstür",
|
||||||
|
"description": "Yaklaşan maçları, canlı skorları ve yapay zeka tahminleriyle geçmiş fikstürleri görüntüleyin."
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Hakkımızda",
|
||||||
|
"description": "iddaai.com, yapay zeka teknolojimiz ve bahis öngörülerini nasıl sağladığımız hakkında daha fazla bilgi edinin."
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Kullanıcı Paneli",
|
||||||
|
"description": "Bahis istatistikleri, tahminler ve hesaba genel bakış için kişiselleştirilmiş paneliniz."
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Profilim",
|
||||||
|
"description": "Kullanıcı profilinizi, aboneliğinizi ve hesap ayarlarınızı yönetin."
|
||||||
|
},
|
||||||
|
"spor-toto": {
|
||||||
|
"title": "Spor Toto Tahminleri",
|
||||||
|
"description": "Yapay zeka destekli Spor Toto tahminleri. Muhafazakar, dengeli veya agresif stratejilerle kuponlar oluşturun."
|
||||||
|
},
|
||||||
|
"coupon-builder": {
|
||||||
|
"title": "Yapay Zeka Kupon Oluşturucu",
|
||||||
|
"description": "Gelişmiş yapay zeka ve istatistiksel modelleri kullanarak otomatik olarak optimize edilmiş bahis kuponları oluşturun."
|
||||||
|
},
|
||||||
|
"teams": {
|
||||||
|
"title": "Takım İstatistikleri",
|
||||||
|
"description": "Futbol takımları için detaylı istatistikler, form durumları ve tahmine dayalı modeller."
|
||||||
|
},
|
||||||
|
"coupon-history": {
|
||||||
|
"title": "Kupon Geçmişi",
|
||||||
|
"description": "Geçmişte oluşturduğunuz bahis kuponlarınızı ve performansınızı inceleyin."
|
||||||
|
},
|
||||||
|
"predictions": {
|
||||||
|
"title": "İddaa Tahminleri",
|
||||||
|
"description": "Günlük yapay zeka iddaa tahminleri, değerli oranlar ve yüksek güvenilirlikli maç tüyoları."
|
||||||
|
},
|
||||||
|
"signup": {
|
||||||
|
"title": "Kayıt Ol",
|
||||||
|
"description": "Yapay zeka tahminlerine erişmek için iddaai.com hesabınızı oluşturun."
|
||||||
|
},
|
||||||
|
"signin": {
|
||||||
|
"title": "Giriş Yap",
|
||||||
|
"description": "Yapay zeka tahminlerine ve araçlarına erişmek için iddaai.com hesabınıza giriş yapın."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 580 KiB |
@@ -27,6 +27,32 @@ import { signIn } from "next-auth/react";
|
|||||||
import { toaster } from "@/components/ui/feedback/toaster";
|
import { toaster } from "@/components/ui/feedback/toaster";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "signin";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t("signin.title"),
|
||||||
|
description: t("signin.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email().required(),
|
||||||
password: yup.string().required(),
|
password: yup.string().required(),
|
||||||
|
|||||||
@@ -27,6 +27,32 @@ import { useState } from "react";
|
|||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { toaster } from "@/components/ui/feedback/toaster";
|
import { toaster } from "@/components/ui/feedback/toaster";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "signup";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t("signup.title"),
|
||||||
|
description: t("signup.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
email: yup.string().email().required(),
|
email: yup.string().email().required(),
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "about";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t("about.title"),
|
||||||
|
description: t("about.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function AboutPage() {
|
function AboutPage() {
|
||||||
return <div>AboutPage</div>;
|
return <div>AboutPage</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,29 @@ import { isAdminRole } from "@/lib/auth/roles";
|
|||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "admin";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("admin.title")} | Suggest Bet`,
|
title: t("admin.title"),
|
||||||
description:
|
description: t("admin.description"),
|
||||||
"Admin panel for managing users, monitoring predictions, and system overview.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import AnalysisContent from "@/components/analysis/analysis-content";
|
import AnalysisContent from "@/components/analysis/analysis-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "analysis";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("analysis.title")} | Suggest Bet`,
|
title: t("analysis.title"),
|
||||||
description: "AI-powered multi-match analysis for coupon generation.",
|
description: t("analysis.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import CouponBuilderContent from "@/components/coupons/coupon-builder-content";
|
import CouponBuilderContent from "@/components/coupons/coupon-builder-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "coupon-builder";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("coupons.builder-title")} | Suggest Bet`,
|
title: t("coupon-builder.title"),
|
||||||
description:
|
description: t("coupon-builder.description"),
|
||||||
"Build your coupon with AI-powered suggestions. Choose your strategy and let AI optimize your bets.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import CouponHistoryContent from "@/components/coupons/coupon-history-content";
|
import CouponHistoryContent from "@/components/coupons/coupon-history-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "coupon-history";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("coupons.history-title")} | Suggest Bet`,
|
title: t("coupon-history.title"),
|
||||||
description:
|
description: t("coupon-history.description"),
|
||||||
"View your coupon history, track wins and losses, and analyze your betting performance.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import DashboardContent from "@/components/dashboard/dashboard-content";
|
import DashboardContent from "@/components/dashboard/dashboard-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "dashboard";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("dashboard.title")} | Suggest Bet`,
|
title: t("dashboard.title"),
|
||||||
description:
|
description: t("dashboard.description"),
|
||||||
"Your personalized betting dashboard with predictions, value bets, and match insights.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import H2HContent from "@/components/h2h/h2h-content";
|
import H2HContent from "@/components/h2h/h2h-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "h2h";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("matches.head-to-head")} | Suggest Bet`,
|
title: t("h2h.title"),
|
||||||
description: "Compare two teams and view their head-to-head match history.",
|
description: t("h2h.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import HomeContent from "@/components/home/home-content";
|
import HomeContent from "@/components/home/home-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "home";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("home")} | Suggest Bet`,
|
title: t("home.title"),
|
||||||
description:
|
description: t("home.description"),
|
||||||
"AI-powered betting predictions. Analyze matches, discover value bets, and build winning coupons.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import LeagueDetailContent from "@/components/leagues/league-detail-content";
|
||||||
|
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string; id: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale, id } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
const pathSegment = `leagues/${id}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${t("leagues.title")} - Detay`,
|
||||||
|
description: t("leagues.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function LeagueDetailPage(props: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await props.params;
|
||||||
|
return <LeagueDetailContent leagueId={id} />;
|
||||||
|
}
|
||||||
@@ -1,11 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import LeaguesContent from "@/components/leagues/leagues-content";
|
import LeaguesContent from "@/components/leagues/leagues-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "leagues";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("leagues.title")} | Suggest Bet`,
|
title: t("leagues.title"),
|
||||||
description: "Browse football and basketball leagues, countries, and teams.",
|
description: t("leagues.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import MatchDetailContent from "@/components/matches/match-detail-content";
|
import MatchDetailContent from "@/components/matches/match-detail-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "matches/[id]";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("matches.match-details")} | Suggest Bet`,
|
title: t("matches.title"),
|
||||||
|
description: t("matches.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import MatchesContent from "@/components/matches/matches-content";
|
import MatchesContent from "@/components/matches/matches-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "matches";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("matches.title")} | Suggest Bet`,
|
title: t("matches.title"),
|
||||||
description:
|
description: t("matches.description"),
|
||||||
"Browse and analyze upcoming football and basketball matches with AI predictions.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import PredictionsContent from "@/components/predictions/predictions-content";
|
import PredictionsContent from "@/components/predictions/predictions-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "predictions";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("predictions.title")} | Suggest Bet`,
|
title: t("predictions.title"),
|
||||||
description:
|
description: t("predictions.description"),
|
||||||
"AI-powered match predictions with confidence scores, value bets, and prediction history.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import ProfileContent from "@/components/profile/profile-content";
|
import ProfileContent from "@/components/profile/profile-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "profile";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("profile.title")} | Suggest Bet`,
|
title: t("profile.title"),
|
||||||
description:
|
description: t("profile.description"),
|
||||||
"Manage your profile, view account info, and track your betting statistics.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import SporTotoContent from "@/components/spor-toto/spor-toto-content";
|
import SporTotoContent from "@/components/spor-toto/spor-toto-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "spor-toto";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("spor-toto.title")} | Suggest Bet`,
|
title: t("spor-toto.title"),
|
||||||
description:
|
description: t("spor-toto.description"),
|
||||||
"Spor Toto predictions with AI-powered analysis. Generate optimized system coupons with contrarian parimutuel strategy.",
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import TeamDetailContent from "@/components/teams/team-detail-content";
|
import TeamDetailContent from "@/components/teams/team-detail-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "teams/[id]";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("nav.teams")} | Suggest Bet`,
|
title: t("teams.title"),
|
||||||
|
description: t("teams.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import TeamsContent from "@/components/teams/teams-content";
|
import TeamsContent from "@/components/teams/teams-content";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
import { Metadata } from "next";
|
||||||
const t = await getTranslations();
|
|
||||||
|
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { locale } = params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
// Next.js parses route variables automatically, but for canonical we'll just use a clean relative base if available,
|
||||||
|
// or let next.js construct it implicitly from metadataBase if not explicitly specified.
|
||||||
|
// We'll set alternates just for languages based on current path segment as a best effort
|
||||||
|
const pathSegment = "teams";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${t("nav.teams")} | Suggest Bet`,
|
title: t("teams.title"),
|
||||||
description: "Search and explore football teams, view match history and stats.",
|
description: t("teams.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,38 @@ import { hasLocale, NextIntlClientProvider } from "next-intl";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
import { dir } from "i18next";
|
import { dir } from "i18next";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||||
|
const { locale } = await params;
|
||||||
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadataBase: new URL(siteUrl),
|
||||||
|
title: {
|
||||||
|
template: `%s | ${t("global.title").split(" | ")[0]}`,
|
||||||
|
default: t("global.title"),
|
||||||
|
},
|
||||||
|
description: t("global.description"),
|
||||||
|
keywords: t("global.keywords"),
|
||||||
|
openGraph: {
|
||||||
|
title: t("global.title"),
|
||||||
|
description: t("global.description"),
|
||||||
|
siteName: t("global.title").split(" | ")[0],
|
||||||
|
locale: locale,
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: t("global.title"),
|
||||||
|
description: t("global.description"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const bricolage = Bricolage_Grotesque({
|
const bricolage = Bricolage_Grotesque({
|
||||||
variable: "--font-bricolage",
|
variable: "--font-bricolage",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -23,6 +53,27 @@ export default async function RootLayout({
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
const jsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "iddaai.com",
|
||||||
|
"url": siteUrl,
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": `${siteUrl}/search?q={search_term_string}`,
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const orgJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "iddaai.com",
|
||||||
|
"url": siteUrl,
|
||||||
|
"logo": `${siteUrl}/favicon/android-chrome-512x512.png`,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang={locale}
|
lang={locale}
|
||||||
@@ -35,6 +86,14 @@ export default async function RootLayout({
|
|||||||
<link rel='icon' type='image/png' sizes='32x32' href='/favicon/favicon-32x32.png' />
|
<link rel='icon' type='image/png' sizes='32x32' href='/favicon/favicon-32x32.png' />
|
||||||
<link rel='icon' type='image/png' sizes='16x16' href='/favicon/favicon-16x16.png' /> */}
|
<link rel='icon' type='image/png' sizes='16x16' href='/favicon/favicon-16x16.png' /> */}
|
||||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={bricolage.variable}>
|
<body className={bricolage.variable}>
|
||||||
<NextIntlClientProvider>
|
<NextIntlClientProvider>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://iddaai.com';
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
disallow: ['/admin', '/*/dashboard', '/*/profile', '/*/coupon-history'],
|
||||||
|
},
|
||||||
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
import { routing } from '@/i18n/routing';
|
||||||
|
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://iddaai.com';
|
||||||
|
|
||||||
|
const staticPages = [
|
||||||
|
'',
|
||||||
|
'/home',
|
||||||
|
'/about',
|
||||||
|
'/analysis',
|
||||||
|
'/leagues',
|
||||||
|
'/matches',
|
||||||
|
'/teams',
|
||||||
|
'/predictions',
|
||||||
|
'/spor-toto',
|
||||||
|
'/coupon-builder',
|
||||||
|
'/h2h'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const sitemapEntries: MetadataRoute.Sitemap = [];
|
||||||
|
|
||||||
|
staticPages.forEach((page) => {
|
||||||
|
routing.locales.forEach((locale) => {
|
||||||
|
sitemapEntries.push({
|
||||||
|
url: `${baseUrl}/${locale}${page}`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: page === '' || page === '/home' ? 1.0 : 0.8,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sitemapEntries;
|
||||||
|
}
|
||||||
@@ -163,7 +163,7 @@ export function LoginModal({ open, onOpenChange, initialMode = "login" }: LoginM
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Heading size="lg" color="primary.500">
|
<Heading as="span" size="lg" color="primary.500">
|
||||||
{mode === "login" ? t("auth.sign-in") : t("auth.sign-up")}
|
{mode === "login" ? t("auth.sign-in") : t("auth.sign-up")}
|
||||||
</Heading>
|
</Heading>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ import { LoginModal } from "@/components/auth/login-modal";
|
|||||||
import { isAdminRole } from "@/lib/auth/roles";
|
import { isAdminRole } from "@/lib/auth/roles";
|
||||||
import { LuLogIn, LuUser, LuShield, LuZap } from "react-icons/lu";
|
import { LuLogIn, LuUser, LuShield, LuZap } from "react-icons/lu";
|
||||||
import GlobalSearch from "@/components/search/global-search";
|
import GlobalSearch from "@/components/search/global-search";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isSticky, setIsSticky] = useState(false);
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
const [loginModalMode, setLoginModalMode] = useState<"login" | "register">("login");
|
const [loginModalMode, setLoginModalMode] = useState<"login" | "register">(
|
||||||
|
"login",
|
||||||
|
);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
|
|
||||||
@@ -227,36 +230,22 @@ export default function Header() {
|
|||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
mr={6}
|
mr={6}
|
||||||
>
|
>
|
||||||
<Flex
|
<img
|
||||||
boxSize="32px"
|
src="/logo.png"
|
||||||
bg="primary.500"
|
alt="iddaai logo"
|
||||||
borderRadius="lg"
|
width={36}
|
||||||
align="center"
|
height={36}
|
||||||
justify="center"
|
style={{ objectFit: "contain" }}
|
||||||
shadow="sm"
|
/>
|
||||||
>
|
|
||||||
<LuZap color="white" size={18} />
|
|
||||||
</Flex>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text
|
<Text
|
||||||
fontSize="md"
|
fontSize="xl"
|
||||||
fontWeight="800"
|
fontWeight="900"
|
||||||
lineHeight="1"
|
lineHeight="1"
|
||||||
color={{ base: "gray.900", _dark: "white" }}
|
color={{ base: "gray.900", _dark: "white" }}
|
||||||
letterSpacing="-0.02em"
|
letterSpacing="-0.04em"
|
||||||
>
|
>
|
||||||
Suggest
|
iddaai
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="600"
|
|
||||||
lineHeight="1"
|
|
||||||
mt="1px"
|
|
||||||
color={{ base: "primary.600", _dark: "primary.300" }}
|
|
||||||
letterSpacing="0.08em"
|
|
||||||
textTransform="uppercase"
|
|
||||||
>
|
|
||||||
BET
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
@@ -325,7 +314,11 @@ export default function Header() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Login Modal */}
|
{/* Login Modal */}
|
||||||
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} initialMode={loginModalMode} />
|
<LoginModal
|
||||||
|
open={loginModalOpen}
|
||||||
|
onOpenChange={setLoginModalOpen}
|
||||||
|
initialMode={loginModalMode}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Box, Flex, Heading, Text, VStack, HStack, Badge, Spinner } from "@chakra-ui/react";
|
||||||
|
import { Link as ChakraLink } from "@chakra-ui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { SlideUp } from "@/components/motion";
|
||||||
|
import { useLeagueById } from "@/lib/api/leagues/use-hooks";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { matchesService } from "@/lib/api/matches/service";
|
||||||
|
import MatchList from "@/components/matches/match-list";
|
||||||
|
import { LuTrophy, LuMapPin, LuArrowLeft } from "react-icons/lu";
|
||||||
|
import { Link } from "@/i18n/navigation";
|
||||||
|
|
||||||
|
export default function LeagueDetailContent({ leagueId }: { leagueId: string }) {
|
||||||
|
const t = useTranslations("leagues");
|
||||||
|
|
||||||
|
const leagueQuery = useLeagueById(leagueId);
|
||||||
|
const league = leagueQuery.data?.data;
|
||||||
|
|
||||||
|
const matchesQuery = useQuery({
|
||||||
|
queryKey: ["league-matches", leagueId, league?.sport],
|
||||||
|
queryFn: () => matchesService.queryMatches({
|
||||||
|
sport: league?.sport || "football",
|
||||||
|
leagueId: leagueId,
|
||||||
|
status: "Finished",
|
||||||
|
limit: 100,
|
||||||
|
}),
|
||||||
|
enabled: !!league,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bgGradient = useColorModeValue(
|
||||||
|
"linear(to-r, primary.500, primary.700)",
|
||||||
|
"linear(to-r, primary.600, primary.900)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const flatMatches = matchesQuery.data?.data?.[0]?.matches || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box minH="calc(100vh - 80px)">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
|
||||||
|
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
|
||||||
|
<LuTrophy size={400} />
|
||||||
|
</Box>
|
||||||
|
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
||||||
|
<SlideUp>
|
||||||
|
<VStack align="flex-start" gap={4} maxW="3xl">
|
||||||
|
<ChakraLink as={Link} href="/leagues" color="whiteAlpha.900" _hover={{ color: "white" }} display="flex" alignItems="center" gap={2} mb={2} fontWeight="medium">
|
||||||
|
<LuArrowLeft /> Liglere Dön
|
||||||
|
</ChakraLink>
|
||||||
|
|
||||||
|
{leagueQuery.isLoading ? (
|
||||||
|
<Spinner color="white" borderWidth="3px" size="xl" />
|
||||||
|
) : league ? (
|
||||||
|
<>
|
||||||
|
<HStack gap={3}>
|
||||||
|
<Badge colorScheme={league.sport === "football" ? "green" : "orange"} variant="solid" bg="whiteAlpha.300" size="lg" px={4} py={1} rounded="full">
|
||||||
|
{league.sport}
|
||||||
|
</Badge>
|
||||||
|
{league.season && (
|
||||||
|
<Badge variant="outline" color="white" borderColor="whiteAlpha.400" size="lg" px={4} py={1} rounded="full">
|
||||||
|
SEZON: {league.season}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
|
||||||
|
{league.name}
|
||||||
|
</Heading>
|
||||||
|
<HStack fontSize="lg" color="whiteAlpha.900">
|
||||||
|
<LuMapPin />
|
||||||
|
<Text>{league.country?.name || "Global"}</Text>
|
||||||
|
</HStack>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Heading>Lig Bulunamadı</Heading>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</SlideUp>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
|
||||||
|
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
|
||||||
|
<Box bg={useColorModeValue("white", "gray.900")} p={{ base: 4, md: 8 }} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={useColorModeValue("gray.200", "gray.800")}>
|
||||||
|
<Heading size="md" mb={6}>Geçmiş Maçlar</Heading>
|
||||||
|
<MatchList flatMatches={flatMatches} isLoading={matchesQuery.isLoading || leagueQuery.isLoading} />
|
||||||
|
</Box>
|
||||||
|
</SlideUp>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Spinner,
|
Spinner,
|
||||||
Input,
|
Input,
|
||||||
Tabs,
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
Icon,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
@@ -22,8 +24,8 @@ import {
|
|||||||
useSearchTeams,
|
useSearchTeams,
|
||||||
} from "@/lib/api/leagues/use-hooks";
|
} from "@/lib/api/leagues/use-hooks";
|
||||||
import type { CountryDto, LeagueDto, TeamDto } from "@/lib/api/leagues/types";
|
import type { CountryDto, LeagueDto, TeamDto } from "@/lib/api/leagues/types";
|
||||||
import { LuSearch, LuGlobe, LuTrophy, LuUsers } from "react-icons/lu";
|
import { LuSearch, LuGlobe, LuTrophy, LuUsers, LuArrowRight, LuMapPin } from "react-icons/lu";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useDebounce } from "@/hooks/use-debounce";
|
import { useDebounce } from "@/hooks/use-debounce";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
import { InputGroup } from "@/components/ui/forms/input-group";
|
import { InputGroup } from "@/components/ui/forms/input-group";
|
||||||
@@ -33,13 +35,24 @@ export default function LeaguesContent() {
|
|||||||
const t = useTranslations("leagues");
|
const t = useTranslations("leagues");
|
||||||
const tMatches = useTranslations("matches");
|
const tMatches = useTranslations("matches");
|
||||||
|
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const bgGradient = useColorModeValue(
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
"linear(to-r, primary.500, primary.700)",
|
||||||
|
"linear(to-r, primary.600, primary.900)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardBg = useColorModeValue("white", "gray.900");
|
||||||
|
const borderColor = useColorModeValue("gray.200", "gray.800");
|
||||||
|
const hoverBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<"leagues" | "teams">("leagues");
|
const [activeTab, setActiveTab] = useState<"leagues" | "teams">("leagues");
|
||||||
const [sportFilter, setSportFilter] = useState<string>("");
|
const [sportFilter, setSportFilter] = useState<string>("");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [selectedCountryId, setSelectedCountryId] = useState<string | null>(null);
|
||||||
const debouncedQuery = useDebounce(searchQuery, 300);
|
|
||||||
|
const [teamSearchQuery, setTeamSearchQuery] = useState("");
|
||||||
|
const debouncedTeamQuery = useDebounce(teamSearchQuery, 300);
|
||||||
|
|
||||||
|
const [countrySearchQuery, setCountrySearchQuery] = useState("");
|
||||||
|
const debouncedCountryQuery = useDebounce(countrySearchQuery, 300);
|
||||||
|
|
||||||
const countries = useCountries();
|
const countries = useCountries();
|
||||||
const leagues = useLeagues(
|
const leagues = useLeagues(
|
||||||
@@ -48,288 +61,379 @@ export default function LeaguesContent() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
const searchTeams = useSearchTeams(
|
const searchTeams = useSearchTeams(
|
||||||
debouncedQuery.length >= 2 ? { q: debouncedQuery } : { q: "" },
|
debouncedTeamQuery.length >= 2 ? { q: debouncedTeamQuery } : { q: "" },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const filteredCountries = useMemo(() => {
|
||||||
|
if (!countries.data?.data) return [];
|
||||||
|
if (!debouncedCountryQuery) return countries.data.data;
|
||||||
|
return countries.data.data.filter((c) =>
|
||||||
|
c.name.toLowerCase().includes(debouncedCountryQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [countries.data?.data, debouncedCountryQuery]);
|
||||||
|
|
||||||
|
const displayedLeagues = useMemo(() => {
|
||||||
|
let sourceLeagues: LeagueDto[] = leagues.data?.data || [];
|
||||||
|
|
||||||
|
if (selectedCountryId) {
|
||||||
|
sourceLeagues = sourceLeagues.filter(l => l.countryId === selectedCountryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sport filter if selected
|
||||||
|
if (sportFilter) {
|
||||||
|
return sourceLeagues.filter(l => l.sport === sportFilter);
|
||||||
|
}
|
||||||
|
return sourceLeagues;
|
||||||
|
}, [selectedCountryId, leagues.data?.data, sportFilter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlideUp>
|
<Box minH="calc(100vh - 80px)">
|
||||||
<Box maxW="6xl" mx="auto">
|
{/* Hero Section */}
|
||||||
<Heading as="h1" size="xl" fontWeight="bold" mb={6}>
|
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
|
||||||
{t("title")}
|
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
|
||||||
</Heading>
|
<LuTrophy size={400} />
|
||||||
|
</Box>
|
||||||
|
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
||||||
|
<SlideUp>
|
||||||
|
<VStack align="center" gap={4} textAlign="center" maxW="3xl" mx="auto">
|
||||||
|
<Badge colorScheme="whiteAlpha" variant="subtle" size="lg" px={4} py={1} rounded="full">
|
||||||
|
{t("title")}
|
||||||
|
</Badge>
|
||||||
|
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
|
||||||
|
{activeTab === "leagues" ? t("countries-leagues") : tMatches("search-teams")}
|
||||||
|
</Heading>
|
||||||
|
<Text fontSize="lg" color="whiteAlpha.800" maxW="xl">
|
||||||
|
{activeTab === "leagues"
|
||||||
|
? "Explore top football and basketball leagues around the world. Filter by country and analyze historical matches."
|
||||||
|
: "Find your favorite teams across all leagues. Get deep insights and head-to-head statistics."}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</SlideUp>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Tabs.Root
|
{/* Main Content Area - Pulled up to overlap hero */}
|
||||||
value={activeTab}
|
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
|
||||||
onValueChange={(e) => setActiveTab(e.value as "leagues" | "teams")}
|
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
|
||||||
>
|
<Card.Root bg={cardBg} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={borderColor} overflow="hidden">
|
||||||
<Tabs.List>
|
|
||||||
<Tabs.Trigger value="leagues">
|
|
||||||
<LuGlobe />
|
|
||||||
{t("countries-leagues")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="teams">
|
|
||||||
<LuUsers />
|
|
||||||
{tMatches("search-teams")}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
|
|
||||||
{/* Countries & Leagues Tab */}
|
{/* Tab Navigation */}
|
||||||
<Tabs.Content value="leagues">
|
<Flex borderBottomWidth="1px" borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
|
||||||
<Flex gap={6} direction={{ base: "column", lg: "row" }}>
|
<Flex flex={1}>
|
||||||
{/* Countries Sidebar */}
|
<Box
|
||||||
<Box w={{ base: "full", lg: "280px" }} flexShrink={0}>
|
flex={1} py={4} textAlign="center" cursor="pointer"
|
||||||
<Card.Root
|
borderBottomWidth="2px"
|
||||||
bg={cardBg}
|
borderColor={activeTab === "leagues" ? "primary.500" : "transparent"}
|
||||||
borderColor={borderColor}
|
color={activeTab === "leagues" ? "primary.500" : "fg.muted"}
|
||||||
borderRadius="xl"
|
fontWeight={activeTab === "leagues" ? "bold" : "medium"}
|
||||||
|
onClick={() => setActiveTab("leagues")}
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{ bg: hoverBg }}
|
||||||
>
|
>
|
||||||
<Card.Header>
|
<HStack justify="center" gap={2}>
|
||||||
<Heading as="h4" size="sm">
|
<LuGlobe />
|
||||||
<HStack gap={2}>
|
<Text>{t("countries-leagues")}</Text>
|
||||||
<LuGlobe />
|
</HStack>
|
||||||
<Text>{t("countries")}</Text>
|
</Box>
|
||||||
</HStack>
|
<Box
|
||||||
</Heading>
|
flex={1} py={4} textAlign="center" cursor="pointer"
|
||||||
</Card.Header>
|
borderBottomWidth="2px"
|
||||||
<Card.Body pt={0} maxH="600px" overflowY="auto">
|
borderColor={activeTab === "teams" ? "primary.500" : "transparent"}
|
||||||
{countries.isLoading ? (
|
color={activeTab === "teams" ? "primary.500" : "fg.muted"}
|
||||||
<Flex justify="center" py={4}>
|
fontWeight={activeTab === "teams" ? "bold" : "medium"}
|
||||||
<Spinner size="sm" />
|
onClick={() => setActiveTab("teams")}
|
||||||
</Flex>
|
transition="all 0.2s"
|
||||||
) : (
|
_hover={{ bg: hoverBg }}
|
||||||
<VStack gap={1} align="stretch">
|
>
|
||||||
{countries.data?.data?.map((country: CountryDto) => (
|
<HStack justify="center" gap={2}>
|
||||||
<Flex
|
<LuUsers />
|
||||||
key={country.id}
|
<Text>{tMatches("search-teams")}</Text>
|
||||||
px={3}
|
</HStack>
|
||||||
py={2}
|
</Box>
|
||||||
borderRadius="md"
|
</Flex>
|
||||||
_hover={{
|
</Flex>
|
||||||
bg: "gray.50",
|
|
||||||
_dark: { bg: "gray.750" },
|
{/* LEAGUES TAB */}
|
||||||
}}
|
{activeTab === "leagues" && (
|
||||||
cursor="pointer"
|
<Flex direction={{ base: "column", lg: "row" }} minH="600px">
|
||||||
justify="space-between"
|
|
||||||
align="center"
|
{/* Left Sidebar: Countries */}
|
||||||
|
<Box w={{ base: "full", lg: "320px" }} borderRightWidth={{ lg: "1px" }} borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
|
||||||
|
<VStack align="stretch" h="full" gap={0}>
|
||||||
|
<Box p={4} borderBottomWidth="1px" borderColor={borderColor} bg={cardBg}>
|
||||||
|
<InputGroup startElement={<LuSearch color="gray.400" />} w="full">
|
||||||
|
<Input
|
||||||
|
placeholder={t("countries") + "..."}
|
||||||
|
variant="subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
value={countrySearchQuery}
|
||||||
|
onChange={(e) => setCountrySearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box flex={1} overflowY="auto" maxH={{ base: "300px", lg: "600px" }} p={2}>
|
||||||
|
{countries.isLoading ? (
|
||||||
|
<Flex justify="center" py={10}><Spinner color="primary.500" /></Flex>
|
||||||
|
) : (
|
||||||
|
<VStack gap={1} align="stretch">
|
||||||
|
<Box
|
||||||
|
px={4} py={3} borderRadius="lg" cursor="pointer"
|
||||||
|
bg={selectedCountryId === null ? "primary.500" : "transparent"}
|
||||||
|
color={selectedCountryId === null ? "white" : "fg"}
|
||||||
|
_hover={{ bg: selectedCountryId === null ? "primary.600" : hoverBg }}
|
||||||
|
onClick={() => setSelectedCountryId(null)}
|
||||||
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
<HStack gap={2}>
|
<HStack justify="space-between">
|
||||||
{country.flag ? (
|
<HStack gap={3}>
|
||||||
<img
|
<LuGlobe />
|
||||||
src={country.flag}
|
<Text fontWeight={selectedCountryId === null ? "bold" : "medium"}>{t("all")}</Text>
|
||||||
width="16"
|
</HStack>
|
||||||
height="16"
|
<Badge size="sm" bg={selectedCountryId === null ? "whiteAlpha.300" : "gray.100"} color={selectedCountryId === null ? "white" : "fg"}>
|
||||||
style={{ borderRadius: "2px" }}
|
{leagues.data?.data?.length || 0}
|
||||||
alt={country.name}
|
</Badge>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<Text fontSize="sm">{country.name}</Text>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge size="xs" colorScheme="gray">
|
</Box>
|
||||||
{country.leagues?.length || 0}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</Card.Body>
|
|
||||||
</Card.Root>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Leagues List */}
|
{filteredCountries.map((country: CountryDto) => {
|
||||||
<Box flex={1}>
|
const isSelected = selectedCountryId === country.id;
|
||||||
<Card.Root
|
return (
|
||||||
bg={cardBg}
|
<Box
|
||||||
borderColor={borderColor}
|
key={country.id}
|
||||||
borderRadius="xl"
|
px={4} py={3} borderRadius="lg" cursor="pointer"
|
||||||
>
|
bg={isSelected ? "primary.500" : "transparent"}
|
||||||
<Card.Header>
|
color={isSelected ? "white" : "fg"}
|
||||||
<Flex justify="space-between" align="center">
|
_hover={{ bg: isSelected ? "primary.600" : hoverBg }}
|
||||||
<Heading as="h4" size="sm">
|
onClick={() => setSelectedCountryId(country.id)}
|
||||||
<HStack gap={2}>
|
transition="all 0.2s"
|
||||||
<LuTrophy />
|
>
|
||||||
<Text>{t("leagues")}</Text>
|
<HStack justify="space-between">
|
||||||
</HStack>
|
<HStack gap={3}>
|
||||||
</Heading>
|
{country.flag ? (
|
||||||
<HStack gap={2}>
|
<img src={country.flag} width="20" height="20" style={{ borderRadius: "50%", objectFit: "cover" }} alt={country.name} />
|
||||||
<Badge
|
) : <LuMapPin />}
|
||||||
cursor="pointer"
|
<Text fontWeight={isSelected ? "bold" : "medium"}>{country.name}</Text>
|
||||||
colorScheme={!sportFilter ? "primary" : "gray"}
|
</HStack>
|
||||||
onClick={() => setSportFilter("")}
|
<Badge size="sm" bg={isSelected ? "whiteAlpha.300" : "gray.100"} color={isSelected ? "white" : "fg"}>
|
||||||
>
|
{leagues.data?.data?.filter(l => l.countryId === country.id).length || 0}
|
||||||
{tMatches("all")}
|
</Badge>
|
||||||
</Badge>
|
</HStack>
|
||||||
<Badge
|
</Box>
|
||||||
cursor="pointer"
|
);
|
||||||
colorScheme={
|
})}
|
||||||
sportFilter === "football" ? "green" : "gray"
|
</VStack>
|
||||||
}
|
)}
|
||||||
onClick={() =>
|
</Box>
|
||||||
setSportFilter(
|
</VStack>
|
||||||
sportFilter === "football" ? "" : "football",
|
</Box>
|
||||||
)
|
|
||||||
}
|
{/* Right Area: Leagues Grid */}
|
||||||
>
|
<Box flex={1} p={{ base: 4, md: 8 }} bg={cardBg}>
|
||||||
{tMatches("football")}
|
{/* Top Filters */}
|
||||||
</Badge>
|
<Flex justify="space-between" align="center" mb={6} direction={{ base: "column", sm: "row" }} gap={4}>
|
||||||
<Badge
|
<Heading size="md" fontWeight="bold">
|
||||||
cursor="pointer"
|
{selectedCountryId
|
||||||
colorScheme={
|
? `${countries.data?.data?.find(c => c.id === selectedCountryId)?.name} ${t("leagues")}`
|
||||||
sportFilter === "basketball" ? "orange" : "gray"
|
: t("leagues")}
|
||||||
}
|
<Text as="span" color="fg.muted" ml={2} fontWeight="normal" fontSize="sm">
|
||||||
onClick={() =>
|
({displayedLeagues.length})
|
||||||
setSportFilter(
|
</Text>
|
||||||
sportFilter === "basketball" ? "" : "basketball",
|
</Heading>
|
||||||
)
|
|
||||||
}
|
<HStack gap={2} bg={useColorModeValue("gray.100", "gray.800")} p={1} borderRadius="full">
|
||||||
>
|
<Box
|
||||||
{tMatches("basketball")}
|
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
||||||
</Badge>
|
bg={!sportFilter ? "white" : "transparent"}
|
||||||
</HStack>
|
color={!sportFilter ? "black" : "fg.muted"}
|
||||||
|
shadow={!sportFilter ? "sm" : "none"}
|
||||||
|
onClick={() => setSportFilter("")}
|
||||||
|
transition="all 0.2s"
|
||||||
|
_dark={{ bg: !sportFilter ? "gray.600" : "transparent", color: !sportFilter ? "white" : "gray.400" }}
|
||||||
|
>
|
||||||
|
{t("all")}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
||||||
|
bg={sportFilter === "football" ? "green.500" : "transparent"}
|
||||||
|
color={sportFilter === "football" ? "white" : "fg.muted"}
|
||||||
|
shadow={sportFilter === "football" ? "sm" : "none"}
|
||||||
|
onClick={() => setSportFilter(sportFilter === "football" ? "" : "football")}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
{tMatches("football")}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
||||||
|
bg={sportFilter === "basketball" ? "orange.500" : "transparent"}
|
||||||
|
color={sportFilter === "basketball" ? "white" : "fg.muted"}
|
||||||
|
shadow={sportFilter === "basketball" ? "sm" : "none"}
|
||||||
|
onClick={() => setSportFilter(sportFilter === "basketball" ? "" : "basketball")}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
{tMatches("basketball")}
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Leagues Grid */}
|
||||||
|
{leagues.isLoading ? (
|
||||||
|
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
|
||||||
|
) : displayedLeagues.length === 0 ? (
|
||||||
|
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
||||||
|
<Box bg="gray.100" _dark={{ bg: "gray.800" }} p={6} borderRadius="full" mb={4}>
|
||||||
|
<LuTrophy size={40} color="gray" />
|
||||||
|
</Box>
|
||||||
|
<Heading size="md" mb={2}>Bulunamadı</Heading>
|
||||||
|
<Text color="fg.muted">Seçili kriterlere uygun lig bulunamadı.</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card.Header>
|
) : (
|
||||||
<Card.Body pt={0}>
|
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
|
||||||
{leagues.isLoading ? (
|
{displayedLeagues.map((league: LeagueDto) => (
|
||||||
<Flex justify="center" py={6}>
|
<GridItem key={league.id}>
|
||||||
<Spinner size="sm" />
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<VStack gap={2}>
|
|
||||||
{leagues.data?.data?.map((league: LeagueDto) => (
|
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
key={league.id}
|
|
||||||
as={Link}
|
as={Link}
|
||||||
href="/matches"
|
href={`/leagues/${league.id}`}
|
||||||
p={3}
|
display="block"
|
||||||
borderRadius="md"
|
h="full"
|
||||||
|
p={5}
|
||||||
|
borderRadius="xl"
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
|
bg={cardBg}
|
||||||
_hover={{
|
_hover={{
|
||||||
borderColor: "primary.300",
|
borderColor: "primary.300",
|
||||||
bg: "primary.50",
|
shadow: "md",
|
||||||
_dark: { bg: "gray.750" },
|
transform: "translateY(-2px)",
|
||||||
}}
|
}}
|
||||||
display="flex"
|
transition="all 0.2s"
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
textDecoration="none"
|
textDecoration="none"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
|
data-group
|
||||||
>
|
>
|
||||||
<VStack align="start" gap={0}>
|
<Flex justify="space-between" align="flex-start" mb={4}>
|
||||||
<Text fontWeight="semibold">{league.name}</Text>
|
<Box p={2} borderRadius="lg" bg={league.sport === "football" ? "green.50" : "orange.50"} _dark={{ bg: league.sport === "football" ? "green.900" : "orange.900" }}>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<LuTrophy size={20} color={league.sport === "football" ? "var(--chakra-colors-green-500)" : "var(--chakra-colors-orange-500)"} />
|
||||||
{league.country?.name || ""}
|
</Box>
|
||||||
</Text>
|
<Badge size="sm" variant="subtle" colorScheme={league.sport === "football" ? "green" : "orange"}>
|
||||||
</VStack>
|
{league.sport}
|
||||||
<HStack gap={2}>
|
</Badge>
|
||||||
{league.sport ? (
|
</Flex>
|
||||||
<Badge
|
|
||||||
size="xs"
|
|
||||||
colorScheme={
|
|
||||||
league.sport === "football"
|
|
||||||
? "green"
|
|
||||||
: "orange"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{league.sport}
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
{league.season ? (
|
|
||||||
<Text fontSize="xs" color="fg.muted">
|
|
||||||
{league.season}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</HStack>
|
|
||||||
</ChakraLink>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</Card.Body>
|
|
||||||
</Card.Root>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
{/* Teams Search Tab */}
|
<Heading size="sm" mb={1} lineClamp={1} _groupHover={{ color: "primary.500" }}>
|
||||||
<Tabs.Content value="teams">
|
{league.name}
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
</Heading>
|
||||||
<Card.Body>
|
<HStack color="fg.muted" fontSize="sm" gap={1}>
|
||||||
<InputGroup startElement={<LuSearch />} mb={4}>
|
<LuMapPin size={14} />
|
||||||
<Input
|
<Text lineClamp={1}>{league.country?.name || "Global"}</Text>
|
||||||
placeholder={tMatches("search-teams")}
|
</HStack>
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
{league.season && (
|
||||||
/>
|
<Flex mt={4} pt={4} borderTopWidth="1px" borderColor={borderColor} justify="space-between" align="center">
|
||||||
</InputGroup>
|
<Text fontSize="xs" color="fg.muted" fontWeight="medium">SEZON: {league.season}</Text>
|
||||||
{debouncedQuery.length < 2 ? (
|
<Icon as={LuArrowRight} color="gray.400" _groupHover={{ color: "primary.500", transform: "translateX(4px)" }} transition="all 0.2s" />
|
||||||
<Text color="fg.muted" textAlign="center" py={8}>
|
</Flex>
|
||||||
{t("search-at-least-2")}
|
)}
|
||||||
</Text>
|
</ChakraLink>
|
||||||
|
</GridItem>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TEAMS TAB */}
|
||||||
|
{activeTab === "teams" && (
|
||||||
|
<Box p={{ base: 4, md: 8 }}>
|
||||||
|
<Box maxW="2xl" mx="auto" mb={10}>
|
||||||
|
<InputGroup startElement={<LuSearch color="gray.400" size={20} />} w="full">
|
||||||
|
<Input
|
||||||
|
placeholder={tMatches("search-teams") + "..."}
|
||||||
|
value={teamSearchQuery}
|
||||||
|
onChange={(e) => setTeamSearchQuery(e.target.value)}
|
||||||
|
variant="outline"
|
||||||
|
borderRadius="xl"
|
||||||
|
fontSize="lg"
|
||||||
|
py={6}
|
||||||
|
boxShadow="sm"
|
||||||
|
_focus={{ boxShadow: "0 0 0 2px var(--chakra-colors-primary-500)" }}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{debouncedTeamQuery.length < 2 ? (
|
||||||
|
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
||||||
|
<Box bg="primary.50" _dark={{ bg: "primary.900" }} p={8} borderRadius="full" mb={6}>
|
||||||
|
<LuUsers size={64} color="var(--chakra-colors-primary-500)" />
|
||||||
|
</Box>
|
||||||
|
<Heading size="lg" mb={3}>{t("search-at-least-2")}</Heading>
|
||||||
|
<Text color="fg.muted" maxW="md">
|
||||||
|
Find detailed statistics, upcoming matches, and head-to-head analysis by searching for any team worldwide.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
) : searchTeams.isLoading ? (
|
) : searchTeams.isLoading ? (
|
||||||
<Flex justify="center" py={6}>
|
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
|
||||||
<Spinner size="md" />
|
) : searchTeams.data?.data?.length === 0 ? (
|
||||||
|
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
||||||
|
<Heading size="md" mb={2}>Takım Bulunamadı</Heading>
|
||||||
|
<Text color="fg.muted">"{debouncedTeamQuery}" aramasıyla eşleşen bir takım bulunamadı.</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<VStack gap={2}>
|
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
|
||||||
{searchTeams.data?.data?.map((team: TeamDto) => (
|
{searchTeams.data?.data?.map((team: TeamDto) => (
|
||||||
<ChakraLink
|
<GridItem key={team.id}>
|
||||||
key={team.id}
|
<ChakraLink
|
||||||
as={Link}
|
as={Link}
|
||||||
href={`/teams/${team.id}`}
|
href={`/teams/${team.id}`}
|
||||||
p={3}
|
display="flex"
|
||||||
borderRadius="md"
|
alignItems="center"
|
||||||
borderWidth="1px"
|
p={4}
|
||||||
borderColor={borderColor}
|
borderRadius="xl"
|
||||||
_hover={{
|
borderWidth="1px"
|
||||||
borderColor: "primary.300",
|
borderColor={borderColor}
|
||||||
bg: "primary.50",
|
bg={cardBg}
|
||||||
_dark: { bg: "gray.750" },
|
_hover={{
|
||||||
}}
|
borderColor: "primary.300",
|
||||||
display="flex"
|
shadow: "md",
|
||||||
alignItems="center"
|
transform: "translateY(-2px)",
|
||||||
gap={3}
|
}}
|
||||||
textDecoration="none"
|
transition="all 0.2s"
|
||||||
color="inherit"
|
textDecoration="none"
|
||||||
>
|
color="inherit"
|
||||||
{team.logo ? (
|
data-group
|
||||||
<img
|
|
||||||
src={team.logo}
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
style={{ borderRadius: "50%" }}
|
|
||||||
alt={team.name}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
boxSize="32px"
|
|
||||||
borderRadius="full"
|
|
||||||
bg="gray.200"
|
|
||||||
_dark={{ bg: "gray.600" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<VStack align="start" gap={0}>
|
|
||||||
<Text fontWeight="semibold">{team.name}</Text>
|
|
||||||
<Text fontSize="xs" color="fg.muted">
|
|
||||||
{team.country || ""}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<Badge
|
|
||||||
ml="auto"
|
|
||||||
size="xs"
|
|
||||||
colorScheme={
|
|
||||||
team.sport === "football" ? "green" : "orange"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{team.sport}
|
{team.logo ? (
|
||||||
</Badge>
|
<Box w={12} h={12} borderRadius="full" overflow="hidden" flexShrink={0} mr={4} bg="white" p={1} shadow="sm">
|
||||||
</ChakraLink>
|
<img src={team.logo} width="100%" height="100%" style={{ objectFit: "contain" }} alt={team.name} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Flex w={12} h={12} borderRadius="full" bg="gray.100" _dark={{ bg: "gray.700" }} align="center" justify="center" flexShrink={0} mr={4}>
|
||||||
|
<LuUsers size={20} color="gray" />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VStack align="start" gap={0} flex={1}>
|
||||||
|
<Heading size="sm" lineClamp={1} _groupHover={{ color: "primary.500" }}>{team.name}</Heading>
|
||||||
|
<HStack color="fg.muted" fontSize="xs" gap={1}>
|
||||||
|
<LuMapPin size={12} />
|
||||||
|
<Text lineClamp={1}>{team.country || "Global"}</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Badge ml={2} size="sm" colorScheme={team.sport === "football" ? "green" : "orange"} variant="subtle">
|
||||||
|
{team.sport}
|
||||||
|
</Badge>
|
||||||
|
</ChakraLink>
|
||||||
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Card.Body>
|
</Box>
|
||||||
</Card.Root>
|
)}
|
||||||
</Tabs.Content>
|
</Card.Root>
|
||||||
</Tabs.Root>
|
</SlideUp>
|
||||||
</Box>
|
</Box>
|
||||||
</SlideUp>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ export default function MatchList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedFlatMatches = [...flatMatches].sort(
|
||||||
|
(a, b) => Number(a.mstUtc) - Number(b.mstUtc),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StaggerContainer>
|
<StaggerContainer>
|
||||||
<Grid
|
<Grid
|
||||||
@@ -127,7 +131,7 @@ export default function MatchList({
|
|||||||
}}
|
}}
|
||||||
gap={4}
|
gap={4}
|
||||||
>
|
>
|
||||||
{flatMatches.map((match) => (
|
{sortedFlatMatches.map((match) => (
|
||||||
<StaggerItem key={match.id}>
|
<StaggerItem key={match.id}>
|
||||||
<MatchCard match={match} />
|
<MatchCard match={match} />
|
||||||
</StaggerItem>
|
</StaggerItem>
|
||||||
@@ -148,9 +152,23 @@ export default function MatchList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort leagues by their earliest match, and sort matches within each league
|
||||||
|
const sortedLeagues = [...leagues]
|
||||||
|
.map((league) => ({
|
||||||
|
...league,
|
||||||
|
matches: [...league.matches].sort(
|
||||||
|
(a, b) => Number(a.mstUtc) - Number(b.mstUtc),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const earliestA = Math.min(...a.matches.map((m) => Number(m.mstUtc)));
|
||||||
|
const earliestB = Math.min(...b.matches.map((m) => Number(m.mstUtc)));
|
||||||
|
return earliestA - earliestB;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StaggerContainer>
|
<StaggerContainer>
|
||||||
{leagues.map((league) => (
|
{sortedLeagues.map((league) => (
|
||||||
<StaggerItem key={league.id}>
|
<StaggerItem key={league.id}>
|
||||||
<Box mb={6}>
|
<Box mb={6}>
|
||||||
{/* League Header */}
|
{/* League Header */}
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import {
|
|||||||
useInView,
|
useInView,
|
||||||
type HTMLMotionProps,
|
type HTMLMotionProps,
|
||||||
} from "framer-motion";
|
} from "framer-motion";
|
||||||
import { forwardRef, type ReactNode, useEffect, useRef } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
Key,
|
||||||
|
type ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Shared animation variants
|
// Shared animation variants
|
||||||
@@ -381,34 +388,92 @@ interface SparkleProps {
|
|||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sparkles({ count = 6, color = "rgba(56, 178, 172, 0.6)" }: SparkleProps) {
|
interface SparkleConfig {
|
||||||
|
id: number;
|
||||||
|
size: number;
|
||||||
|
left: number;
|
||||||
|
bottom: number;
|
||||||
|
y: number;
|
||||||
|
duration: number;
|
||||||
|
delay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sparkles({
|
||||||
|
count = 6,
|
||||||
|
color = "rgba(56, 178, 172, 0.6)",
|
||||||
|
}: SparkleProps) {
|
||||||
|
const [sparkles, setSparkles] = useState<SparkleConfig[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newSparkles = Array.from({ length: count }).map((_, i) => ({
|
||||||
|
id: i,
|
||||||
|
size: 4 + Math.random() * 4,
|
||||||
|
left: 10 + Math.random() * 80,
|
||||||
|
bottom: Math.random() * 30,
|
||||||
|
y: -(60 + Math.random() * 80),
|
||||||
|
duration: 2.5 + Math.random() * 2,
|
||||||
|
delay: Math.random() * 3,
|
||||||
|
}));
|
||||||
|
setSparkles(newSparkles);
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
|
if (sparkles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
|
<div
|
||||||
{Array.from({ length: count }).map((_, i) => (
|
style={{
|
||||||
<motion.div
|
position: "absolute",
|
||||||
key={i}
|
inset: 0,
|
||||||
style={{
|
overflow: "hidden",
|
||||||
position: "absolute",
|
pointerEvents: "none",
|
||||||
width: 4 + Math.random() * 4,
|
}}
|
||||||
height: 4 + Math.random() * 4,
|
>
|
||||||
borderRadius: "50%",
|
{sparkles.map(
|
||||||
background: color,
|
(sparkle: {
|
||||||
left: `${10 + Math.random() * 80}%`,
|
id: Key | null | undefined;
|
||||||
bottom: `${Math.random() * 30}%`,
|
size: any;
|
||||||
}}
|
left: any;
|
||||||
animate={{
|
bottom: any;
|
||||||
y: [0, -(60 + Math.random() * 80)],
|
y: string | number | null;
|
||||||
opacity: [0, 1, 1, 0],
|
duration: any;
|
||||||
scale: [0.5, 1, 0.8, 0],
|
delay: any;
|
||||||
}}
|
}) => (
|
||||||
transition={{
|
<motion.div
|
||||||
duration: 2.5 + Math.random() * 2,
|
key={sparkle.id}
|
||||||
repeat: Infinity,
|
style={{
|
||||||
delay: Math.random() * 3,
|
position: "absolute",
|
||||||
ease: "easeOut",
|
width: sparkle.size,
|
||||||
}}
|
height: sparkle.size,
|
||||||
/>
|
borderRadius: "50%",
|
||||||
))}
|
background: color,
|
||||||
|
left: `${sparkle.left}%`,
|
||||||
|
bottom: `${sparkle.bottom}%`,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, sparkle.y],
|
||||||
|
opacity: [0, 1, 1, 0],
|
||||||
|
scale: [0.5, 1, 0.8, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: sparkle.duration,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: sparkle.delay,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
SelectValueText,
|
SelectValueText,
|
||||||
} from '@/components/ui/collections/select';
|
} from '@/components/ui/collections/select';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { createListCollection } from '@chakra-ui/react';
|
import { createListCollection, ClientOnly } from '@chakra-ui/react';
|
||||||
import { usePathname, useRouter } from '@/i18n/navigation';
|
import { usePathname, useRouter } from '@/i18n/navigation';
|
||||||
|
|
||||||
const LocaleSwitcher = () => {
|
const LocaleSwitcher = () => {
|
||||||
@@ -40,27 +40,29 @@ const LocaleSwitcher = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SelectRoot
|
<ClientOnly fallback={<div style={{ height: '32px' }} />}>
|
||||||
disabled={isPending}
|
<SelectRoot
|
||||||
value={[locale]}
|
disabled={isPending}
|
||||||
onValueChange={onSelectChange}
|
value={[locale]}
|
||||||
w={{ base: 'full', lg: '24' }}
|
onValueChange={onSelectChange}
|
||||||
size='sm'
|
w={{ base: 'full', lg: '24' }}
|
||||||
variant='outline'
|
size='sm'
|
||||||
borderRadius='md'
|
variant='outline'
|
||||||
collection={collections}
|
borderRadius='md'
|
||||||
>
|
collection={collections}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValueText placeholder='Select a language' />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValueText placeholder='Select a language' />
|
||||||
<SelectContent zIndex='9999'>
|
</SelectTrigger>
|
||||||
{collections.items.map((collection) => (
|
<SelectContent zIndex='9999'>
|
||||||
<SelectItem key={collection.value} item={collection}>
|
{collections.items.map((collection) => (
|
||||||
{collection.label}
|
<SelectItem key={collection.value} item={collection}>
|
||||||
</SelectItem>
|
{collection.label}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</SelectRoot>
|
</SelectContent>
|
||||||
|
</SelectRoot>
|
||||||
|
</ClientOnly>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+26
-14
@@ -16,15 +16,26 @@ export function getVisibleNavItems(
|
|||||||
items: NavItem[],
|
items: NavItem[],
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
): NavItem[] {
|
): NavItem[] {
|
||||||
return items.filter((item) => {
|
return items
|
||||||
// Hidden items never show in nav
|
.filter((item) => {
|
||||||
if (item.visible === false) return false;
|
// Hidden items never show in nav
|
||||||
// onlyPublic items hide when authenticated
|
if (item.visible === false) return false;
|
||||||
if (item.onlyPublic && isAuthenticated) return false;
|
// onlyPublic items hide when authenticated
|
||||||
// Protected items hide when not authenticated
|
if (item.onlyPublic && isAuthenticated) return false;
|
||||||
if (item.protected && !isAuthenticated) return false;
|
// Protected items hide when not authenticated
|
||||||
return true;
|
if (item.protected && !isAuthenticated) return false;
|
||||||
});
|
return true;
|
||||||
|
})
|
||||||
|
.map((item) => {
|
||||||
|
// Recursively filter children if they exist
|
||||||
|
if (item.children) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children: getVisibleNavItems(item.children, isAuthenticated),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAV_ITEMS: NavItem[] = [
|
export const NAV_ITEMS: NavItem[] = [
|
||||||
@@ -35,8 +46,8 @@ export const NAV_ITEMS: NavItem[] = [
|
|||||||
{ label: "h2h", href: "/h2h", public: true },
|
{ label: "h2h", href: "/h2h", public: true },
|
||||||
|
|
||||||
// Protected — grouped for cleaner nav
|
// Protected — grouped for cleaner nav
|
||||||
{ label: "dashboard", href: "/dashboard", protected: true },
|
{ label: "dashboard", href: "/dashboard", protected: true, visible: false },
|
||||||
{ label: "predictions", href: "/predictions", protected: true },
|
{ label: "predictions", href: "/predictions", protected: true, visible: false },
|
||||||
|
|
||||||
// Coupon dropdown group
|
// Coupon dropdown group
|
||||||
{
|
{
|
||||||
@@ -45,7 +56,7 @@ export const NAV_ITEMS: NavItem[] = [
|
|||||||
protected: true,
|
protected: true,
|
||||||
children: [
|
children: [
|
||||||
{ label: "coupon-builder", href: "/coupon-builder", protected: true },
|
{ label: "coupon-builder", href: "/coupon-builder", protected: true },
|
||||||
{ label: "coupon-history", href: "/coupon-history", protected: true },
|
{ label: "coupon-history", href: "/coupon-history", protected: true, visible: false },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -54,9 +65,10 @@ export const NAV_ITEMS: NavItem[] = [
|
|||||||
label: "tools",
|
label: "tools",
|
||||||
href: "/analysis",
|
href: "/analysis",
|
||||||
protected: true,
|
protected: true,
|
||||||
|
visible: false,
|
||||||
children: [
|
children: [
|
||||||
{ label: "analysis", href: "/analysis", protected: true },
|
{ label: "analysis", href: "/analysis", protected: true, visible: false },
|
||||||
{ label: "spor-toto", href: "/spor-toto", protected: true },
|
{ label: "spor-toto", href: "/spor-toto", protected: true, visible: false },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user