@@ -0,0 +1,29 @@
|
|||||||
|
# ==========================================
|
||||||
|
# IDDAAI - DEVELOPMENT ENVIRONMENT VARIABLES
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# --- FRONTEND (iddaai-fe) ---
|
||||||
|
# Next.js uygulaması için gerekli ayarlar
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:3005/api
|
||||||
|
PORT=3000
|
||||||
|
HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
# --- BACKEND (iddaai-be) ---
|
||||||
|
# NestJS uygulaması için gerekli ayarlar
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Database (Localde PostgreSQL çalışıyorsa)
|
||||||
|
DATABASE_URL="postgresql://iddaai_user:IddaA1_S4crET!@localhost:5432/iddaai_db?schema=public"
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST="localhost"
|
||||||
|
REDIS_PORT="6379"
|
||||||
|
REDIS_PASSWORD="IddaA1_Redis_Pass!"
|
||||||
|
|
||||||
|
# AI Engine
|
||||||
|
AI_ENGINE_URL="http://localhost:8000"
|
||||||
|
|
||||||
|
# JWT Config
|
||||||
|
JWT_SECRET="b7V8jM2wP1L5mQxs2RdfFkAsLpI2oG!w"
|
||||||
|
JWT_ACCESS_EXPIRATION="1d"
|
||||||
@@ -11,12 +11,20 @@ jobs:
|
|||||||
- name: Kodu Cek
|
- name: Kodu Cek
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker Build
|
- name: Ortam Degiskenlerini Olustur
|
||||||
run: |
|
run: |
|
||||||
docker build \
|
echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > .env.production
|
||||||
--build-arg NEXT_PUBLIC_API_URL='https://api.iddaai.com/api' \
|
echo "NEXT_PUBLIC_APP_URL=${{ secrets.NEXT_PUBLIC_APP_URL }}" >> .env.production
|
||||||
--build-arg NEXT_PUBLIC_AUTH_REQUIRED='false' \
|
echo "NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}" >> .env.production
|
||||||
-t iddaai-fe:latest .
|
echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env.production
|
||||||
|
echo "NEXT_PUBLIC_AUTH_REQUIRED=${{ secrets.NEXT_PUBLIC_AUTH_REQUIRED }}" >> .env.production
|
||||||
|
echo "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=${{ secrets.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN }}" >> .env.production
|
||||||
|
echo "NEXT_PUBLIC_PADDLE_ENVIRONMENT=${{ secrets.NEXT_PUBLIC_PADDLE_ENVIRONMENT }}" >> .env.production
|
||||||
|
echo "NEXT_PUBLIC_PADDLE_SELLER_ID=${{ secrets.NEXT_PUBLIC_PADDLE_SELLER_ID }}" >> .env.production
|
||||||
|
cp .env.production .env.development
|
||||||
|
|
||||||
|
- name: Docker Build
|
||||||
|
run: docker build -t iddaai-fe:latest .
|
||||||
|
|
||||||
- name: Eski Konteyneri Sil
|
- name: Eski Konteyneri Sil
|
||||||
run: docker rm -f iddaai-fe || true
|
run: docker rm -f iddaai-fe || true
|
||||||
@@ -29,8 +37,5 @@ jobs:
|
|||||||
--network iddaai_iddaai-network \
|
--network iddaai_iddaai-network \
|
||||||
-p 127.0.0.1:1510:3000 \
|
-p 127.0.0.1:1510:3000 \
|
||||||
-e NODE_ENV=production \
|
-e NODE_ENV=production \
|
||||||
-e NEXT_PUBLIC_API_URL='https://api.iddaai.com/api' \
|
--env-file .env.production \
|
||||||
-e NEXTAUTH_URL='https://iddaai.com' \
|
|
||||||
-e NEXTAUTH_SECRET='fFw34R134jRof1H2jofh2!32hU3gfjA1' \
|
|
||||||
-e NEXT_PUBLIC_AUTH_REQUIRED='false' \
|
|
||||||
iddaai-fe:latest
|
iddaai-fe:latest
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ node_modules
|
|||||||
.next
|
.next
|
||||||
|
|
||||||
.env.local
|
.env.local
|
||||||
|
certificates/
|
||||||
|
|||||||
+15
-12
@@ -10,9 +10,6 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build Next.js app
|
# Build Next.js app
|
||||||
# NEXT_PUBLIC_API_URL should be set during build if used in static generation
|
|
||||||
# For production, we usually point to the domain name
|
|
||||||
ENV NEXT_PUBLIC_API_URL=https://api.iddaai.com/api
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# --- STAGE 2: RUNNER ---
|
# --- STAGE 2: RUNNER ---
|
||||||
@@ -21,16 +18,22 @@ WORKDIR /app
|
|||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Copy only necessary files
|
# Don't run as root
|
||||||
COPY --from=builder /app/package*.json ./
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
COPY --from=builder /app/.next ./.next
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
# Copy standalone build
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/next.config.ts ./
|
# Set permissions for standalone build
|
||||||
# Copy messages for internationalization
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
COPY --from=builder /app/messages ./messages
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
# Start Next.js
|
# Start standalone server
|
||||||
CMD ["npm", "start"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC38ryU+6/sNDQR
|
|
||||||
OL1pWCKrIms5cTMoSS8UOKhVaHB2n5J4f8pIuAX/sNBycI4/9oFztGGp3UIs+HD4
|
|
||||||
dIsOmFZmHJCpAVhivyPD0MeXYq1t6V5VSNMOnH0y7huvWYmFuAWZ9/41QKSkM/an
|
|
||||||
oYQQvcvLkuS0vbrYCr0jmndbfBIF4zYQEPyBR/fcS5W4lFCPUojhXlfSOvWN/tuY
|
|
||||||
MU5RxOzHbC4DuKnj1Mj/WNBVzpN4BNkBqUakrjhQw2DYhFFlCTxez9+gFPfZXtrm
|
|
||||||
EfZjAvOn+BMDUJhNzMZvBoU5rKAEGYmxhT2NHj0rGoEIUPBuiLguvngWl3OXdAQE
|
|
||||||
brdyG507AgMBAAECggEAAJnET+A6hNTuzpeW1r847cIhA4EBH8KNas18jzrWEy0W
|
|
||||||
N1qDeJVRP7J+G8GOVVsitRQDtaBJVQhCpi0LPzL0JUU2m7araTcikMMfw7jIxDEc
|
|
||||||
475nIgcUyZPJd1sdfdhJ/GS46ceaQgcBaS631a4o+jMyl/x+nbH7SCB6/0t6a5Z+
|
|
||||||
7cMe/NoDbypGyEo8sEZw6idHBdogZO1E+aLOBfGTkc42jzfV4UCbcuWtpGa3QHDw
|
|
||||||
scXEIwHRza5XO8kdn064tHb6JWyjkXh3abeyZU2uoOGFyEZxR0FjxxC579pA2LoB
|
|
||||||
qmmXeFo8uVFIs4L0fXSj3ohW1i+I10qSFvFY7SgSIQKBgQDe7RsY5QbuwQxfgeIa
|
|
||||||
R8VNHLC5ux02q3bEqCr8UoPZaop4Ckg7gIiHumU6/YK6Qke0w0XEkAy2Fhv2Vby5
|
|
||||||
RehgmSZ5+LSXpsW3uutgTMOw/4HaXLlW51icPK0rsdBth6AaOI3uX1j3XzSpgXyI
|
|
||||||
6yYQtJnmvDGtNsfkC2+t6uCpWQKBgQDTPTf7kXyBtxti/nLV9z8/15atfJ6lsxVd
|
|
||||||
oVWuaQEPUS3VwfBQYFKX/jhSQlXVbu4GrklMhSG8P0Q6glyjk/NiuhRUbQRFv7cu
|
|
||||||
6TmXSGWfSvkEQdX+xVsA+rfaCNQu6+cFs0ZnK7pqN41LzwRAvdiyCXHiEi2EyqWw
|
|
||||||
GypCWJRUswKBgQCtuDn4kWlwnxHET5PiBPH04Jm7ctwWIVJBeAdfb/H9eLAFUYXu
|
|
||||||
kIBUvOVsLeg0u7fjXpS808CEGQCbWz7hZl/q/w3j2PLqhvTm84u/FLMe+E252642
|
|
||||||
0bvUrNgKB9wzrpAOPuojyzuqMg/408Y3cH/OXt7b1uYjZGArDtptvm5qqQKBgQC1
|
|
||||||
8lgDDshAbnhfZy2AkMtg8RAu9FUuAjeYAzvq0zT/fXvOT5LvmFfr5SOb7tlB0p+h
|
|
||||||
D4PBLjblj1T0VI74spoD4qVaJuB0N3LQLEDXxpsJfqlIenCZVmJRUKMFYW9pzvWZ
|
|
||||||
WlZ8zRRvItRIhNJz9VHt3+bAw8mDRI08R9m5ddSlswKBgGETkel47kg3l1oR++9s
|
|
||||||
RExiQgTPM9mnFXMJhXpTKqTFZ7ETrNCQMui/ghbnBSpGmYRzrQEsftEMIp9rU7Z4
|
|
||||||
q6m0F28CtJd3QUazE4t/Y62gUrTpQYGpW9fNqjtY8tEyzjxae5cY3zssB49yYfpQ
|
|
||||||
h2KRQnPO3vzLdJMq+PRp2//o
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEXTCCAsWgAwIBAgIRAJwh1nDeNCaehj5TSbwpPpEwDQYJKoZIhvcNAQELBQAw
|
|
||||||
gYkxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEvMC0GA1UECwwmcGl0
|
|
||||||
b25AUGl0b25zLU1hY0Jvb2stQWlyLmxvY2FsIChQaXRvbikxNjA0BgNVBAMMLW1r
|
|
||||||
Y2VydCBwaXRvbkBQaXRvbnMtTWFjQm9vay1BaXIubG9jYWwgKFBpdG9uKTAeFw0y
|
|
||||||
NjA0MTQxMzQ1MjZaFw0yODA3MTQxMzQ1MjZaMFIxJzAlBgNVBAoTHm1rY2VydCBk
|
|
||||||
ZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEnMCUGA1UECwwecGl0b25AUGl0b25zLU1h
|
|
||||||
Y0Jvb2stQWlyLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
|
||||||
t/K8lPuv7DQ0ETi9aVgiqyJrOXEzKEkvFDioVWhwdp+SeH/KSLgF/7DQcnCOP/aB
|
|
||||||
c7Rhqd1CLPhw+HSLDphWZhyQqQFYYr8jw9DHl2KtbeleVUjTDpx9Mu4br1mJhbgF
|
|
||||||
mff+NUCkpDP2p6GEEL3Ly5LktL262Aq9I5p3W3wSBeM2EBD8gUf33EuVuJRQj1KI
|
|
||||||
4V5X0jr1jf7bmDFOUcTsx2wuA7ip49TI/1jQVc6TeATZAalGpK44UMNg2IRRZQk8
|
|
||||||
Xs/foBT32V7a5hH2YwLzp/gTA1CYTczGbwaFOaygBBmJsYU9jR49KxqBCFDwboi4
|
|
||||||
Lr54Fpdzl3QEBG63chudOwIDAQABo3YwdDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0l
|
|
||||||
BAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAU+PxiJ531CXgmujenTGLWFtdGwW8w
|
|
||||||
LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G
|
|
||||||
CSqGSIb3DQEBCwUAA4IBgQAMw+91DrGNCdqLngTCvG8fPU6ikAOBNvuB7Q0tf/q4
|
|
||||||
adfgTse/pU7e9lkgChdYSYifh3FStmkaHmYNZg1ljgpMJICUxT2zL7rmOE9GlUqv
|
|
||||||
2/umlzZcHE3yC3fLqS8Ik7D5qhAES0HM3WbJLrs4OzRY77iEkDYptgzmZJqMA72j
|
|
||||||
CEyfuqRaAMR/QR0D4Lrt8xZlrMA19t8mkdc0GixzlKD0naIISbVyNmXz4Dc2uqv2
|
|
||||||
asGWByPm/m4UmocO9rBX/WlylqC7hLffKRiO1sXdIYWjc2GyGCWt5MrVBanXyXFz
|
|
||||||
SElBFF5XJbVY5gtw+9sGWXyDOiLaTVOd55Td5Rf1Lst6QKWMMk3vdpUAIXMciAPh
|
|
||||||
UiAipbDFwl5Vxjri/nZoCuQWlEOQ6rthKDZJz4qAu4GN1WFeB8pgIPHKkGA9v6Nn
|
|
||||||
1ZvnewsNqq6jYy9WUE/Y4NgZPtdoH8dHQiKav7KXu2yVpbR0iaDJP8oRUNhiE8fe
|
|
||||||
x41Iim7YWjwoYtc97L194WQ=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
+5
-11
@@ -1,20 +1,14 @@
|
|||||||
import { dirname } from 'path';
|
import nextConfig from 'eslint-config-next';
|
||||||
import { fileURLToPath } from 'url';
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
import { FlatCompat } from '@eslint/eslintrc';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
});
|
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
|
...nextConfig,
|
||||||
|
prettierConfig,
|
||||||
{
|
{
|
||||||
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
|
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-empty-object-type': 'off',
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"bet-summary": "Bet Summary",
|
"bet-summary": "Bet Summary",
|
||||||
"expected-value": "Expected Value",
|
"expected-value": "Expected Value",
|
||||||
"no-predictions": "No predictions available.",
|
"no-predictions": "No predictions available.",
|
||||||
|
"generate": "Analyze with AI",
|
||||||
"accuracy": "Accuracy",
|
"accuracy": "Accuracy",
|
||||||
"total-predictions": "Total Predictions",
|
"total-predictions": "Total Predictions",
|
||||||
"correct-predictions": "Correct Predictions",
|
"correct-predictions": "Correct Predictions",
|
||||||
@@ -497,6 +498,10 @@
|
|||||||
"no-users": "No users found."
|
"no-users": "No users found."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"limits": {
|
||||||
|
"analysis_left": "Analyses",
|
||||||
|
"out_of_analysis": "Daily analysis limit exceeded."
|
||||||
|
},
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@@ -594,6 +599,89 @@
|
|||||||
"signin": {
|
"signin": {
|
||||||
"title": "Sign In",
|
"title": "Sign In",
|
||||||
"description": "Sign in to your iddaai.com account to access AI predictions and tools."
|
"description": "Sign in to your iddaai.com account to access AI predictions and tools."
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Pricing — iddaai",
|
||||||
|
"description": "Explore iddaai AI-powered betting analysis plans. Free, Plus, and Premium plans available."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Choose Your Plan",
|
||||||
|
"subtitle": "Boost your winning odds with AI-powered analyses",
|
||||||
|
"monthly": "Monthly",
|
||||||
|
"yearly": "Yearly",
|
||||||
|
"yearly-save": "Save 2 months",
|
||||||
|
"most-popular": "Most Popular",
|
||||||
|
"current-plan": "Current Plan",
|
||||||
|
"get-started": "Get Started",
|
||||||
|
"upgrade": "Upgrade",
|
||||||
|
"downgrade": "Downgrade",
|
||||||
|
"contact-sales": "Contact Us",
|
||||||
|
"per-month": "/mo",
|
||||||
|
"per-year": "/yr",
|
||||||
|
"free-forever": "Free forever",
|
||||||
|
"billed-yearly": "Billed yearly",
|
||||||
|
"compare-plans": "Compare Plans",
|
||||||
|
"faq-title": "Frequently Asked Questions",
|
||||||
|
"plan": {
|
||||||
|
"free": {
|
||||||
|
"name": "Free",
|
||||||
|
"description": "Get started with basic AI analyses"
|
||||||
|
},
|
||||||
|
"plus": {
|
||||||
|
"name": "Plus",
|
||||||
|
"description": "More analyses and exclusive features"
|
||||||
|
},
|
||||||
|
"premium": {
|
||||||
|
"name": "Premium",
|
||||||
|
"description": "Unlimited access and professional tools"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"daily-analyses": "Daily AI analyses",
|
||||||
|
"daily-coupons": "Daily coupons",
|
||||||
|
"basic-analysis": "Basic match analysis",
|
||||||
|
"detailed-analysis": "Detailed AI analysis",
|
||||||
|
"h2h-comparison": "H2H comparison",
|
||||||
|
"coupon-builder": "Coupon builder",
|
||||||
|
"spor-toto": "Spor Toto analysis",
|
||||||
|
"ad-free": "Ad-free experience",
|
||||||
|
"priority-support": "Priority support",
|
||||||
|
"unlimited": "Unlimited"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"q1": "Can I change my plan anytime?",
|
||||||
|
"a1": "Yes, you can upgrade or downgrade your plan anytime. Upgrades take effect immediately.",
|
||||||
|
"q2": "How does cancellation work?",
|
||||||
|
"a2": "Your access continues until the end of your current billing period. You'll automatically switch to the Free plan.",
|
||||||
|
"q3": "What payment methods are accepted?",
|
||||||
|
"a3": "You can pay securely with credit cards and debit cards. All payments are processed through Paddle.",
|
||||||
|
"q4": "Is there a trial period?",
|
||||||
|
"a4": "You can try all basic features with the Free plan. When you upgrade, you get instant access to premium features."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"title": "Subscription",
|
||||||
|
"current-plan": "Current Plan",
|
||||||
|
"plan-badge": {
|
||||||
|
"free": "Free",
|
||||||
|
"plus": "Plus",
|
||||||
|
"premium": "Premium"
|
||||||
|
},
|
||||||
|
"upgrade-cta": "Upgrade Plan",
|
||||||
|
"manage": "Manage Subscription",
|
||||||
|
"cancel": "Cancel Subscription",
|
||||||
|
"cancel-confirm-title": "Cancel Subscription",
|
||||||
|
"cancel-confirm-message": "Your access will continue until the end of your current billing period. Are you sure you want to cancel?",
|
||||||
|
"cancel-reason-placeholder": "Would you like to share your reason? (Optional)",
|
||||||
|
"cancelled-info": "Your subscription ends on {date}",
|
||||||
|
"next-billing": "Next billing date",
|
||||||
|
"usage": {
|
||||||
|
"title": "Daily Usage",
|
||||||
|
"analyses": "AI Analyses",
|
||||||
|
"coupons": "Coupons",
|
||||||
|
"of": "/",
|
||||||
|
"remaining": "remaining"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"bet-summary": "Bahis Özeti",
|
"bet-summary": "Bahis Özeti",
|
||||||
"expected-value": "Beklenen Değer",
|
"expected-value": "Beklenen Değer",
|
||||||
"no-predictions": "Tahmin bulunmuyor.",
|
"no-predictions": "Tahmin bulunmuyor.",
|
||||||
|
"generate": "Yapay Zeka ile Analiz Et",
|
||||||
"accuracy": "Doğruluk",
|
"accuracy": "Doğruluk",
|
||||||
"total-predictions": "Toplam Tahmin",
|
"total-predictions": "Toplam Tahmin",
|
||||||
"correct-predictions": "Doğru Tahmin",
|
"correct-predictions": "Doğru Tahmin",
|
||||||
@@ -487,6 +488,10 @@
|
|||||||
"no-users": "Kullanıcı bulunamadı."
|
"no-users": "Kullanıcı bulunamadı."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
|
"limits": {
|
||||||
|
"analysis_left": "Analiz",
|
||||||
|
"out_of_analysis": "Günlük analiz limitiniz doldu."
|
||||||
|
},
|
||||||
"loading": "Yükleniyor...",
|
"loading": "Yükleniyor...",
|
||||||
"save": "Kaydet",
|
"save": "Kaydet",
|
||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
@@ -584,6 +589,89 @@
|
|||||||
"signin": {
|
"signin": {
|
||||||
"title": "Giriş Yap",
|
"title": "Giriş Yap",
|
||||||
"description": "Yapay zeka tahminlerine ve araçlarına erişmek için iddaai.com hesabınıza giriş yapın."
|
"description": "Yapay zeka tahminlerine ve araçlarına erişmek için iddaai.com hesabınıza giriş yapın."
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Fiyatlandırma — iddaai",
|
||||||
|
"description": "iddaai AI destekli iddaa analiz planlarını keşfedin. Ücretsiz, Plus ve Premium planlar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pricing": {
|
||||||
|
"title": "Planınızı Seçin",
|
||||||
|
"subtitle": "AI destekli analizlerle kazanma şansınızı artırın",
|
||||||
|
"monthly": "Aylık",
|
||||||
|
"yearly": "Yıllık",
|
||||||
|
"yearly-save": "2 ay tasarruf",
|
||||||
|
"most-popular": "En Popüler",
|
||||||
|
"current-plan": "Mevcut Plan",
|
||||||
|
"get-started": "Başla",
|
||||||
|
"upgrade": "Yükselt",
|
||||||
|
"downgrade": "Düşür",
|
||||||
|
"contact-sales": "Bize Ulaşın",
|
||||||
|
"per-month": "/ay",
|
||||||
|
"per-year": "/yıl",
|
||||||
|
"free-forever": "Sonsuza kadar ücretsiz",
|
||||||
|
"billed-yearly": "Yıllık faturalandırılır",
|
||||||
|
"compare-plans": "Planları Karşılaştır",
|
||||||
|
"faq-title": "Sıkça Sorulan Sorular",
|
||||||
|
"plan": {
|
||||||
|
"free": {
|
||||||
|
"name": "Ücretsiz",
|
||||||
|
"description": "Temel AI analizleri ile başlayın"
|
||||||
|
},
|
||||||
|
"plus": {
|
||||||
|
"name": "Plus",
|
||||||
|
"description": "Daha fazla analiz ve özel özellikler"
|
||||||
|
},
|
||||||
|
"premium": {
|
||||||
|
"name": "Premium",
|
||||||
|
"description": "Sınırsız erişim ve profesyonel araçlar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"daily-analyses": "Günlük AI analiz",
|
||||||
|
"daily-coupons": "Günlük kupon",
|
||||||
|
"basic-analysis": "Temel maç analizi",
|
||||||
|
"detailed-analysis": "Detaylı AI analizi",
|
||||||
|
"h2h-comparison": "H2H karşılaştırma",
|
||||||
|
"coupon-builder": "Kupon oluşturucu",
|
||||||
|
"spor-toto": "Spor Toto analizi",
|
||||||
|
"ad-free": "Reklamsız deneyim",
|
||||||
|
"priority-support": "Öncelikli destek",
|
||||||
|
"unlimited": "Sınırsız"
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"q1": "Planımı istediğim zaman değiştirebilir miyim?",
|
||||||
|
"a1": "Evet, planınızı istediğiniz zaman yükseltebilir veya düşürebilirsiniz. Yükseltmeler anında aktif olur.",
|
||||||
|
"q2": "İptal nasıl çalışır?",
|
||||||
|
"a2": "Mevcut fatura döneminizin sonuna kadar erişiminiz devam eder. Otomatik olarak Ücretsiz plana geçersiniz.",
|
||||||
|
"q3": "Ödeme yöntemleri nelerdir?",
|
||||||
|
"a3": "Kredi kartı ve banka kartı ile güvenli ödeme yapabilirsiniz. Tüm ödemeler Paddle altyapısı ile işlenir.",
|
||||||
|
"q4": "Deneme süresi var mı?",
|
||||||
|
"a4": "Ücretsiz plan ile tüm temel özellikleri deneyebilirsiniz. Yükseltme yaptığınızda anında premium özelliklere erişirsiniz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"subscription": {
|
||||||
|
"title": "Abonelik Bilgileri",
|
||||||
|
"current-plan": "Mevcut Plan",
|
||||||
|
"plan-badge": {
|
||||||
|
"free": "Ücretsiz",
|
||||||
|
"plus": "Plus",
|
||||||
|
"premium": "Premium"
|
||||||
|
},
|
||||||
|
"upgrade-cta": "Planı Yükselt",
|
||||||
|
"manage": "Aboneliği Yönet",
|
||||||
|
"cancel": "Aboneliği İptal Et",
|
||||||
|
"cancel-confirm-title": "Aboneliği İptal Et",
|
||||||
|
"cancel-confirm-message": "Mevcut fatura döneminizin sonuna kadar erişiminiz devam edecek. İptal etmek istediğinizden emin misiniz?",
|
||||||
|
"cancel-reason-placeholder": "İptal nedeninizi paylaşır mısınız? (Opsiyonel)",
|
||||||
|
"cancelled-info": "Aboneliğiniz {date} tarihinde sona erecek",
|
||||||
|
"next-billing": "Sonraki fatura tarihi",
|
||||||
|
"usage": {
|
||||||
|
"title": "Günlük Kullanım",
|
||||||
|
"analyses": "AI Analiz",
|
||||||
|
"coupons": "Kupon",
|
||||||
|
"of": "/",
|
||||||
|
"remaining": "kalan"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ const nextConfig: NextConfig = {
|
|||||||
optimizePackageImports: ["@chakra-ui/react"],
|
optimizePackageImports: ["@chakra-ui/react"],
|
||||||
},
|
},
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/(.*)",
|
||||||
|
headers: [
|
||||||
|
{ key: "X-Frame-Options", value: "DENY" },
|
||||||
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||||
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||||
|
{
|
||||||
|
key: "Strict-Transport-Security",
|
||||||
|
value: "max-age=31536000; includeSubDomains",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
|
|||||||
Generated
+185
-199
@@ -12,16 +12,18 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@google/genai": "^1.35.0",
|
"@google/genai": "^1.35.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@paddle/paddle-js": "^1.6.4",
|
||||||
"@tanstack/react-query": "^5.90.16",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"framer-motion": "^12.34.1",
|
"framer-motion": "^12.34.1",
|
||||||
"i18next": "^25.6.0",
|
"i18next": "^25.6.0",
|
||||||
"next": "16.0.0",
|
"next": "^16.2.5",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
|
"postcss": "^8.5.14",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.65.0",
|
||||||
@@ -147,7 +149,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -516,7 +517,6 @@
|
|||||||
"version": "3.33.0",
|
"version": "3.33.0",
|
||||||
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.33.0.tgz",
|
||||||
"integrity": "sha512-HNbUFsFABjVL5IHBxsqtuT+AH/vQT1+xsEWrxnG0GBM2VjlzlMqlqCxNiDyQOsjLZXQC1ciCMbzPNcSCc63Y9w==",
|
"integrity": "sha512-HNbUFsFABjVL5IHBxsqtuT+AH/vQT1+xsEWrxnG0GBM2VjlzlMqlqCxNiDyQOsjLZXQC1ciCMbzPNcSCc63Y9w==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ark-ui/react": "^5.31.0",
|
"@ark-ui/react": "^5.31.0",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
@@ -627,7 +627,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/memoize": "^0.9.0"
|
"@emotion/memoize": "^0.9.0"
|
||||||
}
|
}
|
||||||
@@ -641,7 +640,6 @@
|
|||||||
"version": "11.14.0",
|
"version": "11.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -1280,51 +1278,30 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/ecma402-abstract": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/fast-memoize": "3.1.0",
|
|
||||||
"@formatjs/intl-localematcher": "0.8.1",
|
|
||||||
"decimal.js": "^10.6.0",
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@formatjs/fast-memoize": {
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.4.tgz",
|
||||||
"integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==",
|
"integrity": "sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA=="
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
"version": "3.5.1",
|
"version": "3.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.7.tgz",
|
||||||
"integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==",
|
"integrity": "sha512-wJxRZ+SiUCIMTL86bQlZU9bEKDQqqvgk2ezQ1BySUdWRfHqOzj4IKUVFeUZKS9w58M4e7wMSG0Sl86LAPb7Qww==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "3.1.1",
|
"@formatjs/icu-skeleton-parser": "2.1.7"
|
||||||
"@formatjs/icu-skeleton-parser": "2.1.1",
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.7.tgz",
|
||||||
"integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==",
|
"integrity": "sha512-cIw1SFP0bi0CUBiJ2jzp99ws3OJNQDfStcHq9Z0iHWzItmiIikihFO+npR8C80yDlp7ZuBCLXCcKjgWjHicksA=="
|
||||||
"dependencies": {
|
|
||||||
"@formatjs/ecma402-abstract": "3.1.1",
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@formatjs/intl-localematcher": {
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.6.tgz",
|
||||||
"integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==",
|
"integrity": "sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "3.1.0",
|
"@formatjs/fast-memoize": "3.1.4"
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google/genai": {
|
"node_modules/@google/genai": {
|
||||||
@@ -1853,7 +1830,6 @@
|
|||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
|
||||||
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
|
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/helpers": "^0.5.0"
|
"@swc/helpers": "^0.5.0"
|
||||||
}
|
}
|
||||||
@@ -1936,9 +1912,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.5.tgz",
|
||||||
"integrity": "sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA=="
|
"integrity": "sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA=="
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
"version": "16.0.0",
|
"version": "16.0.0",
|
||||||
@@ -1950,9 +1926,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.5.tgz",
|
||||||
"integrity": "sha512-/CntqDCnk5w2qIwMiF0a9r6+9qunZzFmU0cBX4T82LOflE72zzH6gnOjCwUXYKOBlQi8OpP/rMj8cBIr18x4TA==",
|
"integrity": "sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1965,9 +1941,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.5.tgz",
|
||||||
"integrity": "sha512-hB4GZnJGKa8m4efvTGNyii6qs76vTNl+3dKHTCAUaksN6KjYy4iEO3Q5ira405NW2PKb3EcqWiRaL9DrYJfMHg==",
|
"integrity": "sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1980,9 +1956,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.5.tgz",
|
||||||
"integrity": "sha512-E2IHMdE+C1k+nUgndM13/BY/iJY9KGCphCftMh7SXWcaQqExq/pJU/1Hgn8n/tFwSoLoYC/yUghOv97tAsIxqg==",
|
"integrity": "sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1995,9 +1971,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.5.tgz",
|
||||||
"integrity": "sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg==",
|
"integrity": "sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2010,9 +1986,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.5.tgz",
|
||||||
"integrity": "sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg==",
|
"integrity": "sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2025,9 +2001,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.5.tgz",
|
||||||
"integrity": "sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg==",
|
"integrity": "sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2040,9 +2016,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.5.tgz",
|
||||||
"integrity": "sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA==",
|
"integrity": "sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2055,9 +2031,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.5.tgz",
|
||||||
"integrity": "sha512-O1cJbT+lZp+cTjYyZGiDwsOjO3UHHzSqobkPNipdlnnuPb1swfcuY6r3p8dsKU4hAIEO4cO67ZCfVVH/M1ETXA==",
|
"integrity": "sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2113,6 +2089,11 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@paddle/paddle-js": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@paddle/paddle-js/-/paddle-js-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-ncfnS6I8mCX6krZ3Sgz2iAYivGmhdI81yt9mT6prtPj4Ipd9J3M12LCJRUFL4FB7BYeeuV04c33RSEnbZUBCaA=="
|
||||||
|
},
|
||||||
"node_modules/@pandacss/is-valid-prop": {
|
"node_modules/@pandacss/is-valid-prop": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.8.1.tgz",
|
||||||
@@ -2408,9 +2389,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@parcel/watcher/node_modules/picomatch": {
|
"node_modules/@parcel/watcher/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2438,9 +2419,9 @@
|
|||||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/codegen": {
|
"node_modules/@protobufjs/codegen": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
||||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/eventemitter": {
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -2462,9 +2443,9 @@
|
|||||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/inquire": {
|
"node_modules/@protobufjs/inquire": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz",
|
||||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
"integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew=="
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/path": {
|
"node_modules/@protobufjs/path": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
@@ -2477,9 +2458,9 @@
|
|||||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/utf8": {
|
"node_modules/@protobufjs/utf8": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
||||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="
|
||||||
},
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -2705,7 +2686,6 @@
|
|||||||
"version": "0.5.18",
|
"version": "0.5.18",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@@ -2857,7 +2837,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -2913,7 +2892,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
|
||||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.0",
|
"@typescript-eslint/scope-manager": "8.56.0",
|
||||||
"@typescript-eslint/types": "8.56.0",
|
"@typescript-eslint/types": "8.56.0",
|
||||||
@@ -3052,21 +3030,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
@@ -4248,7 +4226,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -4274,9 +4251,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -4548,13 +4525,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.5",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
@@ -4585,7 +4562,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
|
||||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
}
|
}
|
||||||
@@ -4618,7 +4594,6 @@
|
|||||||
"version": "2.9.19",
|
"version": "2.9.19",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
@@ -4632,9 +4607,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -4672,7 +4647,6 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -5025,11 +4999,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decimal.js": {
|
|
||||||
"version": "10.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
|
||||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="
|
|
||||||
},
|
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -5386,7 +5355,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5579,7 +5547,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5961,15 +5928,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.3",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -6246,19 +6213,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^2.0.1"
|
"brace-expansion": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
@@ -6562,9 +6529,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/icu-minify": {
|
"node_modules/icu-minify": {
|
||||||
"version": "4.8.3",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.11.0.tgz",
|
||||||
"integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==",
|
"integrity": "sha512-XRvblCwLqWXio5ZLcmDqXvJv7alSACK6UjXuuMOdQWB//d25AQX6xlVlI1FEbc3Q6iPLXXo6HaVLn8LcAFhn1Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -6623,14 +6590,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/intl-messageformat": {
|
"node_modules/intl-messageformat": {
|
||||||
"version": "11.1.2",
|
"version": "11.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.4.tgz",
|
||||||
"integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==",
|
"integrity": "sha512-iKP6+uJXn+XcfRgYfGPE3+mqCoODV2vATrXDLo/YkYgIdelJHJPBEbc0GZThipAYPuk+8QJFiPgOfblU085ABg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/ecma402-abstract": "3.1.1",
|
"@formatjs/fast-memoize": "3.1.4",
|
||||||
"@formatjs/fast-memoize": "3.1.0",
|
"@formatjs/icu-messageformat-parser": "3.5.7"
|
||||||
"@formatjs/icu-messageformat-parser": "3.5.1",
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
@@ -7354,9 +7319,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
@@ -7447,14 +7412,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.0.0",
|
"version": "16.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz",
|
||||||
"integrity": "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg==",
|
"integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==",
|
||||||
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.0.0",
|
"@next/env": "16.2.5",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"styled-jsx": "5.1.6"
|
"styled-jsx": "5.1.6"
|
||||||
@@ -7466,15 +7430,15 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.0.0",
|
"@next/swc-darwin-arm64": "16.2.5",
|
||||||
"@next/swc-darwin-x64": "16.0.0",
|
"@next/swc-darwin-x64": "16.2.5",
|
||||||
"@next/swc-linux-arm64-gnu": "16.0.0",
|
"@next/swc-linux-arm64-gnu": "16.2.5",
|
||||||
"@next/swc-linux-arm64-musl": "16.0.0",
|
"@next/swc-linux-arm64-musl": "16.2.5",
|
||||||
"@next/swc-linux-x64-gnu": "16.0.0",
|
"@next/swc-linux-x64-gnu": "16.2.5",
|
||||||
"@next/swc-linux-x64-musl": "16.0.0",
|
"@next/swc-linux-x64-musl": "16.2.5",
|
||||||
"@next/swc-win32-arm64-msvc": "16.0.0",
|
"@next/swc-win32-arm64-msvc": "16.2.5",
|
||||||
"@next/swc-win32-x64-msvc": "16.0.0",
|
"@next/swc-win32-x64-msvc": "16.2.5",
|
||||||
"sharp": "^0.34.4"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
@@ -7531,9 +7495,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-intl": {
|
"node_modules/next-intl": {
|
||||||
"version": "4.8.3",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.11.0.tgz",
|
||||||
"integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==",
|
"integrity": "sha512-Chp8rgEVUYOX/bCtYy+PXH6lDX3X+GPT9sR9HScHroL283em/4urP9btfdHEMEHJJXdq2W/5wDaDDtWONPdNSA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -7544,16 +7508,15 @@
|
|||||||
"@formatjs/intl-localematcher": "^0.8.1",
|
"@formatjs/intl-localematcher": "^0.8.1",
|
||||||
"@parcel/watcher": "^2.4.1",
|
"@parcel/watcher": "^2.4.1",
|
||||||
"@swc/core": "^1.15.2",
|
"@swc/core": "^1.15.2",
|
||||||
"icu-minify": "^4.8.3",
|
"icu-minify": "^4.11.0",
|
||||||
"negotiator": "^1.0.0",
|
"negotiator": "^1.0.0",
|
||||||
"next-intl-swc-plugin-extractor": "^4.8.3",
|
"next-intl-swc-plugin-extractor": "^4.11.0",
|
||||||
"po-parser": "^2.1.1",
|
"po-parser": "^2.1.1",
|
||||||
"use-intl": "^4.8.3"
|
"use-intl": "^4.11.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
|
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||||
"typescript": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
@@ -7562,9 +7525,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next-intl-swc-plugin-extractor": {
|
"node_modules/next-intl-swc-plugin-extractor": {
|
||||||
"version": "4.8.3",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.11.0.tgz",
|
||||||
"integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg=="
|
"integrity": "sha512-WUGBSxGNd8eQ0rAsJHFmRw2H7+SZAXQIY/HAnYM57JaUsj5D2vx4KOz4zFtXlyKDtsw9awHfgWVvBae2/RDF9A=="
|
||||||
},
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.4.6",
|
"version": "0.4.6",
|
||||||
@@ -7583,6 +7546,33 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next/node_modules/postcss": {
|
||||||
|
"version": "8.4.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nextjs-toploader": {
|
"node_modules/nextjs-toploader": {
|
||||||
"version": "3.9.17",
|
"version": "3.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz",
|
"resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz",
|
||||||
@@ -8034,9 +8024,9 @@
|
|||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -8060,9 +8050,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -8078,9 +8068,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -8090,7 +8080,6 @@
|
|||||||
"version": "10.28.3",
|
"version": "10.28.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
|
||||||
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
@@ -8152,21 +8141,21 @@
|
|||||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz",
|
||||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
"integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
"@protobufjs/base64": "^1.1.2",
|
"@protobufjs/base64": "^1.1.2",
|
||||||
"@protobufjs/codegen": "^2.0.4",
|
"@protobufjs/codegen": "^2.0.5",
|
||||||
"@protobufjs/eventemitter": "^1.1.0",
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
"@protobufjs/fetch": "^1.1.0",
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
"@protobufjs/float": "^1.0.2",
|
"@protobufjs/float": "^1.0.2",
|
||||||
"@protobufjs/inquire": "^1.1.0",
|
"@protobufjs/inquire": "^1.1.1",
|
||||||
"@protobufjs/path": "^1.1.2",
|
"@protobufjs/path": "^1.1.2",
|
||||||
"@protobufjs/pool": "^1.1.0",
|
"@protobufjs/pool": "^1.1.0",
|
||||||
"@protobufjs/utf8": "^1.1.0",
|
"@protobufjs/utf8": "^1.1.1",
|
||||||
"@types/node": ">=13.7.0",
|
"@types/node": ">=13.7.0",
|
||||||
"long": "^5.0.0"
|
"long": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -8180,9 +8169,12 @@
|
|||||||
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="
|
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-memoize": {
|
"node_modules/proxy-memoize": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
@@ -8225,7 +8217,6 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -8234,7 +8225,6 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -8246,7 +8236,6 @@
|
|||||||
"version": "7.71.1",
|
"version": "7.71.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -9093,11 +9082,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -9265,7 +9253,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -9411,9 +9398,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-intl": {
|
"node_modules/use-intl": {
|
||||||
"version": "4.8.3",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.11.0.tgz",
|
||||||
"integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==",
|
"integrity": "sha512-7ILhTLuo3fnSKhoTGDk5X9591pjtWr6qB4inrlvGkN9OEyKhoiG73GZFoLSs68wz3BsSGtoWa62iWvrYEYU+iA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -9423,7 +9410,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/fast-memoize": "^3.1.0",
|
"@formatjs/fast-memoize": "^3.1.0",
|
||||||
"@schummar/icu-type-parser": "1.21.5",
|
"@schummar/icu-type-parser": "1.21.5",
|
||||||
"icu-minify": "^4.8.3",
|
"icu-minify": "^4.11.0",
|
||||||
"intl-messageformat": "^11.1.0"
|
"intl-messageformat": "^11.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -9662,9 +9649,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@@ -9697,7 +9684,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -13,16 +13,18 @@
|
|||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@google/genai": "^1.35.0",
|
"@google/genai": "^1.35.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
|
"@paddle/paddle-js": "^1.6.4",
|
||||||
"@tanstack/react-query": "^5.90.16",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"aos": "^2.3.4",
|
"aos": "^2.3.4",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
"framer-motion": "^12.34.1",
|
"framer-motion": "^12.34.1",
|
||||||
"i18next": "^25.6.0",
|
"i18next": "^25.6.0",
|
||||||
"next": "16.0.0",
|
"next": "^16.2.5",
|
||||||
"next-auth": "^4.24.13",
|
"next-auth": "^4.24.13",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "^4.4.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
|
"postcss": "^8.5.14",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.65.0",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Footer from '@/components/layout/footer/footer';
|
import Footer from "@/components/layout/footer/footer";
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from "@chakra-ui/react";
|
||||||
|
|
||||||
function AuthLayout({ children }: { children: React.ReactNode }) {
|
function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Flex minH='100vh' direction='column'>
|
<Flex minH="100vh" direction="column">
|
||||||
<Box as='main'>{children}</Box>
|
<Box as="main">{children}</Box>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export default function CatchAllPage() {
|
export default function CatchAllPage() {
|
||||||
notFound();
|
notFound();
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { notFound } from "next/navigation";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import AnalysisContent from "@/components/analysis/analysis-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import CouponBuilderContent from "@/components/coupons/coupon-builder-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import CouponHistoryContent from "@/components/coupons/coupon-history-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import DashboardContent from "@/components/dashboard/dashboard-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import H2HContent from "@/components/h2h/h2h-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import HomeContent from "@/components/home/home-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { Container, Flex } from '@chakra-ui/react';
|
import { Container, Flex } from "@chakra-ui/react";
|
||||||
import Header from '@/components/layout/header/header';
|
import Header from "@/components/layout/header/header";
|
||||||
import Footer from '@/components/layout/footer/footer';
|
import Footer from "@/components/layout/footer/footer";
|
||||||
import BackToTop from '@/components/ui/back-to-top';
|
import BackToTop from "@/components/ui/back-to-top";
|
||||||
|
|
||||||
function MainLayout({ children }: { children: React.ReactNode }) {
|
function MainLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Flex minH='100vh' direction='column'>
|
<Flex minH="100vh" direction="column">
|
||||||
<Header />
|
<Header />
|
||||||
<Container as='main' maxW='8xl' flex='1' py={4}>
|
<Container as="main" maxW="8xl" flex="1" py={4}>
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import LeagueDetailContent from "@/components/leagues/league-detail-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string; id: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string; id: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale, id } = params;
|
const { locale, id } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
@@ -24,7 +26,9 @@ export async function generateMetadata(props: { params: Promise<{ locale: string
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function LeagueDetailPage(props: { params: Promise<{ id: string }> }) {
|
export default async function LeagueDetailPage(props: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
const { id } = await props.params;
|
const { id } = await props.params;
|
||||||
return <LeagueDetailContent leagueId={id} />;
|
return <LeagueDetailContent leagueId={id} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import LeaguesContent from "@/components/leagues/leagues-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import MatchDetailContent from "@/components/matches/match-detail-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import MatchesContent from "@/components/matches/matches-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import PredictionsContent from "@/components/predictions/predictions-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import PricingContent from "@/components/pricing/pricing-content";
|
||||||
|
|
||||||
|
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";
|
||||||
|
const pathSegment = "pricing";
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t("pricing.title"),
|
||||||
|
description: t("pricing.description"),
|
||||||
|
alternates: {
|
||||||
|
canonical: `${siteUrl}/${locale}/${pathSegment}`,
|
||||||
|
languages: {
|
||||||
|
en: `${siteUrl}/en/${pathSegment}`,
|
||||||
|
tr: `${siteUrl}/tr/${pathSegment}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
return <PricingContent />;
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import ProfileContent from "@/components/profile/profile-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import SporTotoContent from "@/components/spor-toto/spor-toto-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import TeamDetailContent from "@/components/teams/team-detail-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import TeamsContent from "@/components/teams/teams-content";
|
|||||||
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export async function generateMetadata(props: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const { locale } = params;
|
const { locale } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
|
|||||||
+14
-10
@@ -8,7 +8,11 @@ import { Metadata } from "next";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: "seo" });
|
const t = await getTranslations({ locale, namespace: "seo" });
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
@@ -57,21 +61,21 @@ export default async function RootLayout({
|
|||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "WebSite",
|
"@type": "WebSite",
|
||||||
"name": "iddaai.com",
|
name: "iddaai.com",
|
||||||
"url": siteUrl,
|
url: siteUrl,
|
||||||
"potentialAction": {
|
potentialAction: {
|
||||||
"@type": "SearchAction",
|
"@type": "SearchAction",
|
||||||
"target": `${siteUrl}/search?q={search_term_string}`,
|
target: `${siteUrl}/search?q={search_term_string}`,
|
||||||
"query-input": "required name=search_term_string"
|
"query-input": "required name=search_term_string",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const orgJsonLd = {
|
const orgJsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "iddaai.com",
|
name: "iddaai.com",
|
||||||
"url": siteUrl,
|
url: siteUrl,
|
||||||
"logo": `${siteUrl}/favicon/android-chrome-512x512.png`,
|
logo: `${siteUrl}/favicon/android-chrome-512x512.png`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
import { Link } from '@/i18n/navigation';
|
import { Link } from "@/i18n/navigation";
|
||||||
import { Flex, Text, Button, VStack, Heading } from '@chakra-ui/react';
|
import { Flex, Text, Button, VStack, Heading } from "@chakra-ui/react";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export default async function NotFoundPage() {
|
export default async function NotFoundPage() {
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h='100vh' alignItems='center' justifyContent='center' textAlign='center' px={6}>
|
<Flex
|
||||||
|
h="100vh"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
textAlign="center"
|
||||||
|
px={6}
|
||||||
|
>
|
||||||
<VStack spaceY={6}>
|
<VStack spaceY={6}>
|
||||||
<Heading
|
<Heading
|
||||||
as='h1'
|
as="h1"
|
||||||
fontSize={{ base: '5xl', md: '6xl' }}
|
fontSize={{ base: "5xl", md: "6xl" }}
|
||||||
fontWeight='bold'
|
fontWeight="bold"
|
||||||
color={{ base: 'primary.600', _dark: 'primary.400' }}
|
color={{ base: "primary.600", _dark: "primary.400" }}
|
||||||
>
|
>
|
||||||
{t('error.404')}
|
{t("error.404")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text fontSize={{ base: 'md', md: 'lg' }} color={{ base: 'fg.muted', _dark: 'white' }}>
|
<Text
|
||||||
{t('error.not-found')}
|
fontSize={{ base: "md", md: "lg" }}
|
||||||
|
color={{ base: "fg.muted", _dark: "white" }}
|
||||||
|
>
|
||||||
|
{t("error.not-found")}
|
||||||
</Text>
|
</Text>
|
||||||
<Link href='/home' passHref>
|
<Link href="/home" passHref>
|
||||||
<Button size={{ base: 'md', md: 'lg' }} rounded='md'>
|
<Button size={{ base: "md", md: "lg" }} rounded="md">
|
||||||
{t('error.back-to-home')}
|
{t("error.back-to-home")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
redirect('/home');
|
redirect("/home");
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from "next";
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://iddaai.com';
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
userAgent: '*',
|
userAgent: "*",
|
||||||
allow: '/',
|
allow: "/",
|
||||||
disallow: ['/admin', '/*/dashboard', '/*/profile', '/*/coupon-history'],
|
disallow: ["/admin", "/*/dashboard", "/*/profile", "/*/coupon-history"],
|
||||||
},
|
},
|
||||||
sitemap: `${baseUrl}/sitemap.xml`,
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
};
|
};
|
||||||
|
|||||||
+16
-16
@@ -1,20 +1,20 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from "next";
|
||||||
import { routing } from '@/i18n/routing';
|
import { routing } from "@/i18n/routing";
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://iddaai.com';
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||||
|
|
||||||
const staticPages = [
|
const staticPages = [
|
||||||
'',
|
"",
|
||||||
'/home',
|
"/home",
|
||||||
'/about',
|
"/about",
|
||||||
'/analysis',
|
"/analysis",
|
||||||
'/leagues',
|
"/leagues",
|
||||||
'/matches',
|
"/matches",
|
||||||
'/teams',
|
"/teams",
|
||||||
'/predictions',
|
"/predictions",
|
||||||
'/spor-toto',
|
"/spor-toto",
|
||||||
'/coupon-builder',
|
"/coupon-builder",
|
||||||
'/h2h'
|
"/h2h",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
@@ -25,8 +25,8 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
sitemapEntries.push({
|
sitemapEntries.push({
|
||||||
url: `${baseUrl}/${locale}${page}`,
|
url: `${baseUrl}/${locale}${page}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: 'daily',
|
changeFrequency: "daily",
|
||||||
priority: page === '' || page === '/home' ? 1.0 : 0.8,
|
priority: page === "" || page === "/home" ? 1.0 : 0.8,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ export default function AdminContent() {
|
|||||||
<StaggerItem>
|
<StaggerItem>
|
||||||
<AdminStat
|
<AdminStat
|
||||||
label={t("total-users")}
|
label={t("total-users")}
|
||||||
value={analytics?.totalUsers ?? analytics?.users?.total ?? 0}
|
value={
|
||||||
|
analytics?.totalUsers ?? analytics?.users?.total ?? 0
|
||||||
|
}
|
||||||
icon={<LuUsers />}
|
icon={<LuUsers />}
|
||||||
colorPalette="primary"
|
colorPalette="primary"
|
||||||
/>
|
/>
|
||||||
@@ -202,15 +204,27 @@ export default function AdminContent() {
|
|||||||
<StaggerItem>
|
<StaggerItem>
|
||||||
<AdminStat
|
<AdminStat
|
||||||
label={t("total-predictions")}
|
label={t("total-predictions")}
|
||||||
value={analytics?.totalPredictions ?? analytics?.predictions ?? 0}
|
value={
|
||||||
|
analytics?.totalPredictions ?? analytics?.predictions ?? 0
|
||||||
|
}
|
||||||
icon={<LuChartBar />}
|
icon={<LuChartBar />}
|
||||||
colorPalette="green"
|
colorPalette="green"
|
||||||
/>
|
/>
|
||||||
</StaggerItem>
|
</StaggerItem>
|
||||||
|
<StaggerItem>
|
||||||
|
<AdminStat
|
||||||
|
label={t("premium-users", { fallback: "Premium Users" })}
|
||||||
|
value={analytics?.users?.premium ?? 0}
|
||||||
|
icon={<LuShield />}
|
||||||
|
colorPalette="purple"
|
||||||
|
/>
|
||||||
|
</StaggerItem>
|
||||||
<StaggerItem>
|
<StaggerItem>
|
||||||
<AdminStat
|
<AdminStat
|
||||||
label={t("active-users")}
|
label={t("active-users")}
|
||||||
value={analytics?.activeUsers ?? analytics?.users?.active ?? 0}
|
value={
|
||||||
|
analytics?.activeUsers ?? analytics?.users?.active ?? 0
|
||||||
|
}
|
||||||
icon={<LuActivity />}
|
icon={<LuActivity />}
|
||||||
colorPalette="orange"
|
colorPalette="orange"
|
||||||
/>
|
/>
|
||||||
@@ -253,6 +267,9 @@ export default function AdminContent() {
|
|||||||
<Text flex={1} textAlign="center">
|
<Text flex={1} textAlign="center">
|
||||||
{t("user-role")}
|
{t("user-role")}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text flex={1} textAlign="center">
|
||||||
|
{t("subscription", { fallback: "Subscription" })}
|
||||||
|
</Text>
|
||||||
<Text flex={1} textAlign="center">
|
<Text flex={1} textAlign="center">
|
||||||
{t("user-status")}
|
{t("user-status")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -282,7 +299,9 @@ export default function AdminContent() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Flex flex={1} justify="center">
|
<Flex flex={1} justify="center">
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={isAdminRole([user.role]) ? "red" : "gray"}
|
colorPalette={
|
||||||
|
isAdminRole([user.role]) ? "red" : "gray"
|
||||||
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
@@ -290,6 +309,23 @@ export default function AdminContent() {
|
|||||||
{formatRoleLabel(user.role)}
|
{formatRoleLabel(user.role)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Flex flex={1} justify="center">
|
||||||
|
<Badge
|
||||||
|
colorPalette={
|
||||||
|
user.subscriptionStatus === "premium"
|
||||||
|
? "purple"
|
||||||
|
: user.subscriptionStatus === "plus"
|
||||||
|
? "blue"
|
||||||
|
: "gray"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
textTransform="capitalize"
|
||||||
|
>
|
||||||
|
{user.subscriptionStatus || "free"}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
<Flex flex={1} justify="center">
|
<Flex flex={1} justify="center">
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={user.isActive ? "green" : "gray"}
|
colorPalette={user.isActive ? "green" : "gray"}
|
||||||
|
|||||||
@@ -41,12 +41,7 @@ export default function AnalysisContent() {
|
|||||||
const toast = (opts: { title: string; status: string }) =>
|
const toast = (opts: { title: string; status: string }) =>
|
||||||
toaster.create({
|
toaster.create({
|
||||||
title: opts.title,
|
title: opts.title,
|
||||||
type: opts.status as
|
type: opts.status as "success" | "warning" | "error" | "info" | "loading",
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "error"
|
|
||||||
| "info"
|
|
||||||
| "loading",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleMatch = (id: string) => {
|
const toggleMatch = (id: string) => {
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ interface LoginModalProps {
|
|||||||
|
|
||||||
/* ────────────────────────── Component ────────────────────────── */
|
/* ────────────────────────── Component ────────────────────────── */
|
||||||
|
|
||||||
export function LoginModal({ open, onOpenChange, initialMode = "login" }: LoginModalProps) {
|
export function LoginModal({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
initialMode = "login",
|
||||||
|
}: LoginModalProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [mode, setMode] = useState<"login" | "register">(initialMode);
|
const [mode, setMode] = useState<"login" | "register">(initialMode);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|||||||
@@ -769,30 +769,51 @@ export default function CouponBuilderContent() {
|
|||||||
{/* Engine Mode Toggle */}
|
{/* Engine Mode Toggle */}
|
||||||
<VStack align="stretch" gap={2} mb={4}>
|
<VStack align="stretch" gap={2} mb={4}>
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={engineMode === "ai" ? LuSparkles : LuDatabase} color={engineMode === "ai" ? "teal.500" : "cyan.500"} />
|
<Icon
|
||||||
<Text fontWeight="semibold" fontSize="sm">{t("engine-mode-label")}</Text>
|
as={engineMode === "ai" ? LuSparkles : LuDatabase}
|
||||||
<InfoIcon content={t("engine-mode-help")} label={t("engine-mode-label")} />
|
color={engineMode === "ai" ? "teal.500" : "cyan.500"}
|
||||||
|
/>
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">
|
||||||
|
{t("engine-mode-label")}
|
||||||
|
</Text>
|
||||||
|
<InfoIcon
|
||||||
|
content={t("engine-mode-help")}
|
||||||
|
label={t("engine-mode-label")}
|
||||||
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={engineMode === "ai" ? "teal" : "gray"}
|
colorPalette={engineMode === "ai" ? "teal" : "gray"}
|
||||||
variant={engineMode === "ai" ? "solid" : "outline"}
|
variant={engineMode === "ai" ? "solid" : "outline"}
|
||||||
cursor="pointer" px={3} py={1}
|
cursor="pointer"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
onClick={() => setEngineMode("ai")}
|
onClick={() => setEngineMode("ai")}
|
||||||
>
|
>
|
||||||
<LuSparkles /> AI
|
<LuSparkles /> AI
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={engineMode === "frequency" ? "cyan" : "gray"}
|
colorPalette={
|
||||||
variant={engineMode === "frequency" ? "solid" : "outline"}
|
engineMode === "frequency" ? "cyan" : "gray"
|
||||||
cursor="pointer" px={3} py={1}
|
}
|
||||||
|
variant={
|
||||||
|
engineMode === "frequency" ? "solid" : "outline"
|
||||||
|
}
|
||||||
|
cursor="pointer"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
onClick={() => setEngineMode("frequency")}
|
onClick={() => setEngineMode("frequency")}
|
||||||
>
|
>
|
||||||
<LuDatabase /> Frekans
|
<LuDatabase /> Frekans
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="xs" color={engineMode === "ai" ? "teal.500" : "cyan.500"}>
|
<Text
|
||||||
{engineMode === "ai" ? t("ai-mode-active") : t("freq-mode-active")}
|
fontSize="xs"
|
||||||
|
color={engineMode === "ai" ? "teal.500" : "cyan.500"}
|
||||||
|
>
|
||||||
|
{engineMode === "ai"
|
||||||
|
? t("ai-mode-active")
|
||||||
|
: t("freq-mode-active")}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
@@ -819,7 +840,9 @@ export default function CouponBuilderContent() {
|
|||||||
key={entry.key}
|
key={entry.key}
|
||||||
p={3}
|
p={3}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={active ? `${palette}.400` : borderColor}
|
borderColor={
|
||||||
|
active ? `${palette}.400` : borderColor
|
||||||
|
}
|
||||||
bg={active ? `${palette}.50` : mutedBg}
|
bg={active ? `${palette}.50` : mutedBg}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
@@ -832,7 +855,9 @@ export default function CouponBuilderContent() {
|
|||||||
>
|
>
|
||||||
{entry.label}
|
{entry.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
{active ? <LuCheck color="currentColor" /> : null}
|
{active ? (
|
||||||
|
<LuCheck color="currentColor" />
|
||||||
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
{entry.description}
|
{entry.description}
|
||||||
@@ -866,7 +891,9 @@ export default function CouponBuilderContent() {
|
|||||||
min="2"
|
min="2"
|
||||||
max="15"
|
max="15"
|
||||||
value={matchCount}
|
value={matchCount}
|
||||||
onChange={(e) => setMatchCount(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setMatchCount(Number(e.target.value))
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
accentColor: "teal",
|
accentColor: "teal",
|
||||||
@@ -880,7 +907,9 @@ export default function CouponBuilderContent() {
|
|||||||
>
|
>
|
||||||
<Text>2</Text>
|
<Text>2</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{t("match-count-auto", { count: allMatches.length })}
|
{t("match-count-auto", {
|
||||||
|
count: allMatches.length,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>15</Text>
|
<Text>15</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -170,7 +170,11 @@ export default function FrequencyPanel() {
|
|||||||
max="95"
|
max="95"
|
||||||
value={minSignal * 100}
|
value={minSignal * 100}
|
||||||
onChange={(e) => setMinSignal(Number(e.target.value) / 100)}
|
onChange={(e) => setMinSignal(Number(e.target.value) / 100)}
|
||||||
style={{ width: "100%", accentColor: "#0891b2", cursor: "pointer" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
accentColor: "#0891b2",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
||||||
<Text>50%</Text>
|
<Text>50%</Text>
|
||||||
@@ -197,7 +201,11 @@ export default function FrequencyPanel() {
|
|||||||
max="5"
|
max="5"
|
||||||
value={maxMatches}
|
value={maxMatches}
|
||||||
onChange={(e) => setMaxMatches(Number(e.target.value))}
|
onChange={(e) => setMaxMatches(Number(e.target.value))}
|
||||||
style={{ width: "100%", accentColor: "#9333ea", cursor: "pointer" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
accentColor: "#9333ea",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
<HStack justify="space-between" fontSize="xs" color="fg.muted">
|
||||||
<Text>2</Text>
|
<Text>2</Text>
|
||||||
@@ -325,7 +333,12 @@ export default function FrequencyPanel() {
|
|||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
bg={mutedBg}
|
bg={mutedBg}
|
||||||
>
|
>
|
||||||
<Flex justify="space-between" align="flex-start" gap={3} mb={3}>
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
gap={3}
|
||||||
|
mb={3}
|
||||||
|
>
|
||||||
<VStack align="flex-start" gap={1}>
|
<VStack align="flex-start" gap={1}>
|
||||||
<Text fontWeight="bold">{bet.match_name}</Text>
|
<Text fontWeight="bold">{bet.match_name}</Text>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
@@ -405,7 +418,9 @@ export default function FrequencyPanel() {
|
|||||||
<Box p={4} bg="orange.50" borderRadius="xl">
|
<Box p={4} bg="orange.50" borderRadius="xl">
|
||||||
<HStack gap={2} mb={2}>
|
<HStack gap={2} mb={2}>
|
||||||
<Icon as={LuBadgeAlert} color="orange.500" />
|
<Icon as={LuBadgeAlert} color="orange.500" />
|
||||||
<Text fontWeight="semibold">{t("rejected-matches-title")}</Text>
|
<Text fontWeight="semibold">
|
||||||
|
{t("rejected-matches-title")}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<VStack align="stretch" gap={1}>
|
<VStack align="stretch" gap={1}>
|
||||||
{result.rejected_matches.map((entry, i) => (
|
{result.rejected_matches.map((entry, i) => (
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import {
|
|||||||
} 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";
|
||||||
import { SlideUp, StaggerContainer, StaggerItem, ScrollSlideUp } from "@/components/motion";
|
import {
|
||||||
|
SlideUp,
|
||||||
|
StaggerContainer,
|
||||||
|
StaggerItem,
|
||||||
|
ScrollSlideUp,
|
||||||
|
} from "@/components/motion";
|
||||||
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
||||||
import { MatchCard } from "@/components/matches";
|
import { MatchCard } from "@/components/matches";
|
||||||
import { useQueryMatches } from "@/lib/api/matches/use-hooks";
|
import { useQueryMatches } from "@/lib/api/matches/use-hooks";
|
||||||
@@ -26,8 +31,14 @@ import { useUserBettingStats } from "@/lib/api/coupons/use-hooks";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { LuTrendingUp, LuTarget, LuTicket, LuChartBar } from "react-icons/lu";
|
import { LuTrendingUp, LuTarget, LuTicket, LuChartBar } from "react-icons/lu";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { LeagueWithMatchesDto, MatchResponseDto } from "@/lib/api/matches/types";
|
import type {
|
||||||
import type { MatchPredictionDto, ValueBetDto } from "@/lib/api/predictions/types";
|
LeagueWithMatchesDto,
|
||||||
|
MatchResponseDto,
|
||||||
|
} from "@/lib/api/matches/types";
|
||||||
|
import type {
|
||||||
|
MatchPredictionDto,
|
||||||
|
ValueBetDto,
|
||||||
|
} from "@/lib/api/predictions/types";
|
||||||
|
|
||||||
// ========================
|
// ========================
|
||||||
// Stats Card
|
// Stats Card
|
||||||
@@ -181,8 +192,11 @@ export default function DashboardContent() {
|
|||||||
queryMatches.mutate({ sport: "football", limit: 20 });
|
queryMatches.mutate({ sport: "football", limit: 20 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayMatches: MatchResponseDto[] = queryMatches.data?.data?.flatMap((l: LeagueWithMatchesDto) => l.matches) ?? [];
|
const todayMatches: MatchResponseDto[] =
|
||||||
const upcomingPredictions: MatchPredictionDto[] = upcomingData?.data?.matches ?? [];
|
queryMatches.data?.data?.flatMap((l: LeagueWithMatchesDto) => l.matches) ??
|
||||||
|
[];
|
||||||
|
const upcomingPredictions: MatchPredictionDto[] =
|
||||||
|
upcomingData?.data?.matches ?? [];
|
||||||
const valueBets: ValueBetDto[] = valueBetsData?.data ?? [];
|
const valueBets: ValueBetDto[] = valueBetsData?.data ?? [];
|
||||||
const userStats = statsData?.data;
|
const userStats = statsData?.data;
|
||||||
|
|
||||||
@@ -328,7 +342,9 @@ export default function DashboardContent() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
) : upcomingPredictions.length > 0 ? (
|
) : upcomingPredictions.length > 0 ? (
|
||||||
<VStack gap={2} align="stretch">
|
<VStack gap={2} align="stretch">
|
||||||
{upcomingPredictions.slice(0, 4).map((pred: MatchPredictionDto, idx: number) => (
|
{upcomingPredictions
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((pred: MatchPredictionDto, idx: number) => (
|
||||||
<Box
|
<Box
|
||||||
key={idx}
|
key={idx}
|
||||||
p={2.5}
|
p={2.5}
|
||||||
@@ -396,7 +412,9 @@ export default function DashboardContent() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
) : valueBets.length > 0 ? (
|
) : valueBets.length > 0 ? (
|
||||||
<VStack gap={2} align="stretch">
|
<VStack gap={2} align="stretch">
|
||||||
{valueBets.slice(0, 5).map((vb: ValueBetDto, idx: number) => (
|
{valueBets
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((vb: ValueBetDto, idx: number) => (
|
||||||
<ValueBetMiniCard
|
<ValueBetMiniCard
|
||||||
key={idx}
|
key={idx}
|
||||||
matchName={vb.matchName}
|
matchName={vb.matchName}
|
||||||
|
|||||||
@@ -309,7 +309,11 @@ export default function HomeContent() {
|
|||||||
shadow="lg"
|
shadow="lg"
|
||||||
>
|
>
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={6}>
|
<SimpleGrid columns={{ base: 2, md: 4 }} gap={6}>
|
||||||
<StatBlock value={15000} label={t("stats-predictions")} suffix="+" />
|
<StatBlock
|
||||||
|
value={15000}
|
||||||
|
label={t("stats-predictions")}
|
||||||
|
suffix="+"
|
||||||
|
/>
|
||||||
<StatBlock value={72} label={t("stats-accuracy")} suffix="%" />
|
<StatBlock value={72} label={t("stats-accuracy")} suffix="%" />
|
||||||
<StatBlock value={3200} label={t("stats-users")} suffix="+" />
|
<StatBlock value={3200} label={t("stats-users")} suffix="+" />
|
||||||
<StatBlock value={50000} label={t("stats-matches")} suffix="+" />
|
<StatBlock value={50000} label={t("stats-matches")} suffix="+" />
|
||||||
@@ -320,7 +324,13 @@ export default function HomeContent() {
|
|||||||
{/* Features Section */}
|
{/* Features Section */}
|
||||||
<Box mb={16}>
|
<Box mb={16}>
|
||||||
<ScrollScaleIn>
|
<ScrollScaleIn>
|
||||||
<Heading as="h2" size="xl" textAlign="center" mb={3} fontWeight="bold">
|
<Heading
|
||||||
|
as="h2"
|
||||||
|
size="xl"
|
||||||
|
textAlign="center"
|
||||||
|
mb={3}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
{t("features-title")}
|
{t("features-title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ClientOnly,
|
ClientOnly,
|
||||||
Text,
|
Text,
|
||||||
Separator,
|
Separator,
|
||||||
|
Badge,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link, useRouter } from "@/i18n/navigation";
|
import { Link, useRouter } from "@/i18n/navigation";
|
||||||
import { ColorModeButton } from "@/components/ui/color-mode";
|
import { ColorModeButton } from "@/components/ui/color-mode";
|
||||||
@@ -40,9 +41,11 @@ import { signOut, useSession } from "next-auth/react";
|
|||||||
import { authConfig } from "@/config/auth";
|
import { authConfig } from "@/config/auth";
|
||||||
import { LoginModal } from "@/components/auth/login-modal";
|
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, LuCrown } from "react-icons/lu";
|
||||||
|
import { PlanBadge } from "@/components/subscription";
|
||||||
import GlobalSearch from "@/components/search/global-search";
|
import GlobalSearch from "@/components/search/global-search";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useGetMe } from "@/lib/api/users/use-hooks";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -57,6 +60,8 @@ export default function Header() {
|
|||||||
const isAuthenticated = !!session;
|
const isAuthenticated = !!session;
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
const visibleItems = getVisibleNavItems(NAV_ITEMS, isAuthenticated);
|
const visibleItems = getVisibleNavItems(NAV_ITEMS, isAuthenticated);
|
||||||
|
const { data: meData } = useGetMe(isAuthenticated);
|
||||||
|
const usageLimit = meData?.data?.usageLimit;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => setIsSticky(window.scrollY >= 10);
|
const handleScroll = () => setIsSticky(window.scrollY >= 10);
|
||||||
@@ -82,6 +87,24 @@ export default function Header() {
|
|||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
|
<HStack gap={2}>
|
||||||
|
{usageLimit && (
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
colorPalette={
|
||||||
|
usageLimit.maxAnalyses - usageLimit.analysisCount > 0
|
||||||
|
? "green"
|
||||||
|
: "red"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
display={{ base: "none", sm: "inline-flex" }}
|
||||||
|
>
|
||||||
|
<LuZap style={{ marginRight: "4px" }} />
|
||||||
|
{usageLimit.maxAnalyses - usageLimit.analysisCount}{" "}
|
||||||
|
{t("common.limits.analysis_left", { defaultValue: "Analiz" })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<PlanBadge plan={session?.user?.subscriptionPlan ?? "free"} />
|
||||||
<MenuRoot positioning={{ placement: "bottom-start" }}>
|
<MenuRoot positioning={{ placement: "bottom-start" }}>
|
||||||
<MenuTrigger rounded="full" focusRing="none">
|
<MenuTrigger rounded="full" focusRing="none">
|
||||||
<Avatar name={session?.user?.name || "User"} variant="solid" />
|
<Avatar name={session?.user?.name || "User"} variant="solid" />
|
||||||
@@ -91,6 +114,15 @@ export default function Header() {
|
|||||||
<LuUser />
|
<LuUser />
|
||||||
{t("nav.profile")}
|
{t("nav.profile")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{(session?.user?.subscriptionPlan ?? "free") === "free" && (
|
||||||
|
<MenuItem
|
||||||
|
value="pricing"
|
||||||
|
onClick={() => router.push("/pricing")}
|
||||||
|
>
|
||||||
|
<LuCrown />
|
||||||
|
{t("nav.pricing")}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{session?.user && isAdminRole(session.user.roles) && (
|
{session?.user && isAdminRole(session.user.roles) && (
|
||||||
<MenuItem value="admin" onClick={() => router.push("/admin")}>
|
<MenuItem value="admin" onClick={() => router.push("/admin")}>
|
||||||
<LuShield />
|
<LuShield />
|
||||||
@@ -102,6 +134,7 @@ export default function Header() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
</MenuRoot>
|
</MenuRoot>
|
||||||
|
</HStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +176,24 @@ export default function Header() {
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Text fontSize="sm" fontWeight="semibold" truncate>
|
<Text fontSize="sm" fontWeight="semibold" truncate flex={1}>
|
||||||
{session?.user?.name || session?.user?.email}
|
{session?.user?.name || session?.user?.email}
|
||||||
</Text>
|
</Text>
|
||||||
|
{usageLimit && (
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
colorPalette={
|
||||||
|
usageLimit.maxAnalyses - usageLimit.analysisCount > 0
|
||||||
|
? "green"
|
||||||
|
: "red"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<LuZap style={{ marginRight: "4px" }} />
|
||||||
|
{usageLimit.maxAnalyses - usageLimit.analysisCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<PlanBadge plan={session?.user?.subscriptionPlan ?? "free"} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -157,6 +205,18 @@ export default function Header() {
|
|||||||
<LuUser />
|
<LuUser />
|
||||||
{t("nav.profile")}
|
{t("nav.profile")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{(session?.user?.subscriptionPlan ?? "free") === "free" && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
width="full"
|
||||||
|
colorPalette="primary"
|
||||||
|
onClick={() => router.push("/pricing")}
|
||||||
|
>
|
||||||
|
<LuCrown />
|
||||||
|
{t("nav.pricing")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="surface"
|
variant="surface"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Flex, Heading, Text, VStack, HStack, Badge, Spinner } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Badge,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { Link as ChakraLink } from "@chakra-ui/react";
|
import { Link as ChakraLink } 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";
|
||||||
@@ -12,7 +21,11 @@ import MatchList from "@/components/matches/match-list";
|
|||||||
import { LuTrophy, LuMapPin, LuArrowLeft } from "react-icons/lu";
|
import { LuTrophy, LuMapPin, LuArrowLeft } from "react-icons/lu";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
|
|
||||||
export default function LeagueDetailContent({ leagueId }: { leagueId: string }) {
|
export default function LeagueDetailContent({
|
||||||
|
leagueId,
|
||||||
|
}: {
|
||||||
|
leagueId: string;
|
||||||
|
}) {
|
||||||
const t = useTranslations("leagues");
|
const t = useTranslations("leagues");
|
||||||
|
|
||||||
const leagueQuery = useLeagueById(leagueId);
|
const leagueQuery = useLeagueById(leagueId);
|
||||||
@@ -20,7 +33,8 @@ export default function LeagueDetailContent({ leagueId }: { leagueId: string })
|
|||||||
|
|
||||||
const matchesQuery = useQuery({
|
const matchesQuery = useQuery({
|
||||||
queryKey: ["league-matches", leagueId, league?.sport],
|
queryKey: ["league-matches", leagueId, league?.sport],
|
||||||
queryFn: () => matchesService.queryMatches({
|
queryFn: () =>
|
||||||
|
matchesService.queryMatches({
|
||||||
sport: league?.sport || "football",
|
sport: league?.sport || "football",
|
||||||
leagueId: leagueId,
|
leagueId: leagueId,
|
||||||
status: "Finished",
|
status: "Finished",
|
||||||
@@ -31,7 +45,7 @@ export default function LeagueDetailContent({ leagueId }: { leagueId: string })
|
|||||||
|
|
||||||
const bgGradient = useColorModeValue(
|
const bgGradient = useColorModeValue(
|
||||||
"linear(to-r, primary.500, primary.700)",
|
"linear(to-r, primary.500, primary.700)",
|
||||||
"linear(to-r, primary.600, primary.900)"
|
"linear(to-r, primary.600, primary.900)",
|
||||||
);
|
);
|
||||||
|
|
||||||
const flatMatches = matchesQuery.data?.data?.[0]?.matches || [];
|
const flatMatches = matchesQuery.data?.data?.[0]?.matches || [];
|
||||||
@@ -39,14 +53,38 @@ export default function LeagueDetailContent({ leagueId }: { leagueId: string })
|
|||||||
return (
|
return (
|
||||||
<Box minH="calc(100vh - 80px)">
|
<Box minH="calc(100vh - 80px)">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
|
<Box
|
||||||
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
|
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} />
|
<LuTrophy size={400} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
||||||
<SlideUp>
|
<SlideUp>
|
||||||
<VStack align="flex-start" gap={4} maxW="3xl">
|
<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">
|
<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
|
<LuArrowLeft /> Liglere Dön
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
|
|
||||||
@@ -55,16 +93,39 @@ export default function LeagueDetailContent({ leagueId }: { leagueId: string })
|
|||||||
) : league ? (
|
) : league ? (
|
||||||
<>
|
<>
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<Badge colorScheme={league.sport === "football" ? "green" : "orange"} variant="solid" bg="whiteAlpha.300" size="lg" px={4} py={1} rounded="full">
|
<Badge
|
||||||
|
colorScheme={
|
||||||
|
league.sport === "football" ? "green" : "orange"
|
||||||
|
}
|
||||||
|
variant="solid"
|
||||||
|
bg="whiteAlpha.300"
|
||||||
|
size="lg"
|
||||||
|
px={4}
|
||||||
|
py={1}
|
||||||
|
rounded="full"
|
||||||
|
>
|
||||||
{league.sport}
|
{league.sport}
|
||||||
</Badge>
|
</Badge>
|
||||||
{league.season && (
|
{league.season && (
|
||||||
<Badge variant="outline" color="white" borderColor="whiteAlpha.400" size="lg" px={4} py={1} rounded="full">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
color="white"
|
||||||
|
borderColor="whiteAlpha.400"
|
||||||
|
size="lg"
|
||||||
|
px={4}
|
||||||
|
py={1}
|
||||||
|
rounded="full"
|
||||||
|
>
|
||||||
SEZON: {league.season}
|
SEZON: {league.season}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
|
<Heading
|
||||||
|
as="h1"
|
||||||
|
fontSize={{ base: "3xl", md: "5xl" }}
|
||||||
|
fontWeight="800"
|
||||||
|
letterSpacing="tight"
|
||||||
|
>
|
||||||
{league.name}
|
{league.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
<HStack fontSize="lg" color="whiteAlpha.900">
|
<HStack fontSize="lg" color="whiteAlpha.900">
|
||||||
@@ -81,11 +142,33 @@ export default function LeagueDetailContent({ leagueId }: { leagueId: string })
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
|
<Box
|
||||||
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
|
maxW="7xl"
|
||||||
<Box bg={useColorModeValue("white", "gray.900")} p={{ base: 4, md: 8 }} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={useColorModeValue("gray.200", "gray.800")}>
|
mx="auto"
|
||||||
<Heading size="md" mb={6}>Geçmiş Maçlar</Heading>
|
px={6}
|
||||||
<MatchList flatMatches={flatMatches} isLoading={matchesQuery.isLoading || leagueQuery.isLoading} />
|
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>
|
</Box>
|
||||||
</SlideUp>
|
</SlideUp>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ 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, LuArrowRight, LuMapPin } from "react-icons/lu";
|
import {
|
||||||
|
LuSearch,
|
||||||
|
LuGlobe,
|
||||||
|
LuTrophy,
|
||||||
|
LuUsers,
|
||||||
|
LuArrowRight,
|
||||||
|
LuMapPin,
|
||||||
|
} from "react-icons/lu";
|
||||||
import { useMemo, 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";
|
||||||
@@ -37,7 +44,7 @@ export default function LeaguesContent() {
|
|||||||
|
|
||||||
const bgGradient = useColorModeValue(
|
const bgGradient = useColorModeValue(
|
||||||
"linear(to-r, primary.500, primary.700)",
|
"linear(to-r, primary.500, primary.700)",
|
||||||
"linear(to-r, primary.600, primary.900)"
|
"linear(to-r, primary.600, primary.900)",
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardBg = useColorModeValue("white", "gray.900");
|
const cardBg = useColorModeValue("white", "gray.900");
|
||||||
@@ -46,7 +53,9 @@ export default function LeaguesContent() {
|
|||||||
|
|
||||||
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 [selectedCountryId, setSelectedCountryId] = useState<string | null>(null);
|
const [selectedCountryId, setSelectedCountryId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const [teamSearchQuery, setTeamSearchQuery] = useState("");
|
const [teamSearchQuery, setTeamSearchQuery] = useState("");
|
||||||
const debouncedTeamQuery = useDebounce(teamSearchQuery, 300);
|
const debouncedTeamQuery = useDebounce(teamSearchQuery, 300);
|
||||||
@@ -68,7 +77,7 @@ export default function LeaguesContent() {
|
|||||||
if (!countries.data?.data) return [];
|
if (!countries.data?.data) return [];
|
||||||
if (!debouncedCountryQuery) return countries.data.data;
|
if (!debouncedCountryQuery) return countries.data.data;
|
||||||
return countries.data.data.filter((c) =>
|
return countries.data.data.filter((c) =>
|
||||||
c.name.toLowerCase().includes(debouncedCountryQuery.toLowerCase())
|
c.name.toLowerCase().includes(debouncedCountryQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
}, [countries.data?.data, debouncedCountryQuery]);
|
}, [countries.data?.data, debouncedCountryQuery]);
|
||||||
|
|
||||||
@@ -76,12 +85,14 @@ export default function LeaguesContent() {
|
|||||||
let sourceLeagues: LeagueDto[] = leagues.data?.data || [];
|
let sourceLeagues: LeagueDto[] = leagues.data?.data || [];
|
||||||
|
|
||||||
if (selectedCountryId) {
|
if (selectedCountryId) {
|
||||||
sourceLeagues = sourceLeagues.filter(l => l.countryId === selectedCountryId);
|
sourceLeagues = sourceLeagues.filter(
|
||||||
|
(l) => l.countryId === selectedCountryId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sport filter if selected
|
// Apply sport filter if selected
|
||||||
if (sportFilter) {
|
if (sportFilter) {
|
||||||
return sourceLeagues.filter(l => l.sport === sportFilter);
|
return sourceLeagues.filter((l) => l.sport === sportFilter);
|
||||||
}
|
}
|
||||||
return sourceLeagues;
|
return sourceLeagues;
|
||||||
}, [selectedCountryId, leagues.data?.data, sportFilter]);
|
}, [selectedCountryId, leagues.data?.data, sportFilter]);
|
||||||
@@ -89,18 +100,52 @@ export default function LeaguesContent() {
|
|||||||
return (
|
return (
|
||||||
<Box minH="calc(100vh - 80px)">
|
<Box minH="calc(100vh - 80px)">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
|
<Box
|
||||||
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
|
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} />
|
<LuTrophy size={400} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
|
||||||
<SlideUp>
|
<SlideUp>
|
||||||
<VStack align="center" gap={4} textAlign="center" maxW="3xl" mx="auto">
|
<VStack
|
||||||
<Badge colorScheme="whiteAlpha" variant="subtle" size="lg" px={4} py={1} rounded="full">
|
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")}
|
{t("title")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
|
<Heading
|
||||||
{activeTab === "leagues" ? t("countries-leagues") : tMatches("search-teams")}
|
as="h1"
|
||||||
|
fontSize={{ base: "3xl", md: "5xl" }}
|
||||||
|
fontWeight="800"
|
||||||
|
letterSpacing="tight"
|
||||||
|
>
|
||||||
|
{activeTab === "leagues"
|
||||||
|
? t("countries-leagues")
|
||||||
|
: tMatches("search-teams")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text fontSize="lg" color="whiteAlpha.800" maxW="xl">
|
<Text fontSize="lg" color="whiteAlpha.800" maxW="xl">
|
||||||
{activeTab === "leagues"
|
{activeTab === "leagues"
|
||||||
@@ -113,17 +158,42 @@ export default function LeaguesContent() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Main Content Area - Pulled up to overlap hero */}
|
{/* Main Content Area - Pulled up to overlap hero */}
|
||||||
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
|
<Box
|
||||||
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
|
maxW="7xl"
|
||||||
<Card.Root bg={cardBg} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={borderColor} overflow="hidden">
|
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] }}
|
||||||
|
>
|
||||||
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
shadow="xl"
|
||||||
|
borderRadius="2xl"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<Flex borderBottomWidth="1px" borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
|
<Flex
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||||
|
>
|
||||||
<Flex flex={1}>
|
<Flex flex={1}>
|
||||||
<Box
|
<Box
|
||||||
flex={1} py={4} textAlign="center" cursor="pointer"
|
flex={1}
|
||||||
|
py={4}
|
||||||
|
textAlign="center"
|
||||||
|
cursor="pointer"
|
||||||
borderBottomWidth="2px"
|
borderBottomWidth="2px"
|
||||||
borderColor={activeTab === "leagues" ? "primary.500" : "transparent"}
|
borderColor={
|
||||||
|
activeTab === "leagues" ? "primary.500" : "transparent"
|
||||||
|
}
|
||||||
color={activeTab === "leagues" ? "primary.500" : "fg.muted"}
|
color={activeTab === "leagues" ? "primary.500" : "fg.muted"}
|
||||||
fontWeight={activeTab === "leagues" ? "bold" : "medium"}
|
fontWeight={activeTab === "leagues" ? "bold" : "medium"}
|
||||||
onClick={() => setActiveTab("leagues")}
|
onClick={() => setActiveTab("leagues")}
|
||||||
@@ -136,9 +206,14 @@ export default function LeaguesContent() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
flex={1} py={4} textAlign="center" cursor="pointer"
|
flex={1}
|
||||||
|
py={4}
|
||||||
|
textAlign="center"
|
||||||
|
cursor="pointer"
|
||||||
borderBottomWidth="2px"
|
borderBottomWidth="2px"
|
||||||
borderColor={activeTab === "teams" ? "primary.500" : "transparent"}
|
borderColor={
|
||||||
|
activeTab === "teams" ? "primary.500" : "transparent"
|
||||||
|
}
|
||||||
color={activeTab === "teams" ? "primary.500" : "fg.muted"}
|
color={activeTab === "teams" ? "primary.500" : "fg.muted"}
|
||||||
fontWeight={activeTab === "teams" ? "bold" : "medium"}
|
fontWeight={activeTab === "teams" ? "bold" : "medium"}
|
||||||
onClick={() => setActiveTab("teams")}
|
onClick={() => setActiveTab("teams")}
|
||||||
@@ -156,41 +231,92 @@ export default function LeaguesContent() {
|
|||||||
{/* LEAGUES TAB */}
|
{/* LEAGUES TAB */}
|
||||||
{activeTab === "leagues" && (
|
{activeTab === "leagues" && (
|
||||||
<Flex direction={{ base: "column", lg: "row" }} minH="600px">
|
<Flex direction={{ base: "column", lg: "row" }} minH="600px">
|
||||||
|
|
||||||
{/* Left Sidebar: Countries */}
|
{/* Left Sidebar: Countries */}
|
||||||
<Box w={{ base: "full", lg: "320px" }} borderRightWidth={{ lg: "1px" }} borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
|
<Box
|
||||||
|
w={{ base: "full", lg: "320px" }}
|
||||||
|
borderRightWidth={{ lg: "1px" }}
|
||||||
|
borderColor={borderColor}
|
||||||
|
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||||
|
>
|
||||||
<VStack align="stretch" h="full" gap={0}>
|
<VStack align="stretch" h="full" gap={0}>
|
||||||
<Box p={4} borderBottomWidth="1px" borderColor={borderColor} bg={cardBg}>
|
<Box
|
||||||
<InputGroup startElement={<LuSearch color="gray.400" />} w="full">
|
p={4}
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
bg={cardBg}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
startElement={<LuSearch color="gray.400" />}
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("countries") + "..."}
|
placeholder={t("countries") + "..."}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
value={countrySearchQuery}
|
value={countrySearchQuery}
|
||||||
onChange={(e) => setCountrySearchQuery(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setCountrySearchQuery(e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box flex={1} overflowY="auto" maxH={{ base: "300px", lg: "600px" }} p={2}>
|
<Box
|
||||||
|
flex={1}
|
||||||
|
overflowY="auto"
|
||||||
|
maxH={{ base: "300px", lg: "600px" }}
|
||||||
|
p={2}
|
||||||
|
>
|
||||||
{countries.isLoading ? (
|
{countries.isLoading ? (
|
||||||
<Flex justify="center" py={10}><Spinner color="primary.500" /></Flex>
|
<Flex justify="center" py={10}>
|
||||||
|
<Spinner color="primary.500" />
|
||||||
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<VStack gap={1} align="stretch">
|
<VStack gap={1} align="stretch">
|
||||||
<Box
|
<Box
|
||||||
px={4} py={3} borderRadius="lg" cursor="pointer"
|
px={4}
|
||||||
bg={selectedCountryId === null ? "primary.500" : "transparent"}
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
cursor="pointer"
|
||||||
|
bg={
|
||||||
|
selectedCountryId === null
|
||||||
|
? "primary.500"
|
||||||
|
: "transparent"
|
||||||
|
}
|
||||||
color={selectedCountryId === null ? "white" : "fg"}
|
color={selectedCountryId === null ? "white" : "fg"}
|
||||||
_hover={{ bg: selectedCountryId === null ? "primary.600" : hoverBg }}
|
_hover={{
|
||||||
|
bg:
|
||||||
|
selectedCountryId === null
|
||||||
|
? "primary.600"
|
||||||
|
: hoverBg,
|
||||||
|
}}
|
||||||
onClick={() => setSelectedCountryId(null)}
|
onClick={() => setSelectedCountryId(null)}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<LuGlobe />
|
<LuGlobe />
|
||||||
<Text fontWeight={selectedCountryId === null ? "bold" : "medium"}>{t("all")}</Text>
|
<Text
|
||||||
|
fontWeight={
|
||||||
|
selectedCountryId === null
|
||||||
|
? "bold"
|
||||||
|
: "medium"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("all")}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge size="sm" bg={selectedCountryId === null ? "whiteAlpha.300" : "gray.100"} color={selectedCountryId === null ? "white" : "fg"}>
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
bg={
|
||||||
|
selectedCountryId === null
|
||||||
|
? "whiteAlpha.300"
|
||||||
|
: "gray.100"
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
selectedCountryId === null ? "white" : "fg"
|
||||||
|
}
|
||||||
|
>
|
||||||
{leagues.data?.data?.length || 0}
|
{leagues.data?.data?.length || 0}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -201,22 +327,52 @@ export default function LeaguesContent() {
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={country.id}
|
key={country.id}
|
||||||
px={4} py={3} borderRadius="lg" cursor="pointer"
|
px={4}
|
||||||
|
py={3}
|
||||||
|
borderRadius="lg"
|
||||||
|
cursor="pointer"
|
||||||
bg={isSelected ? "primary.500" : "transparent"}
|
bg={isSelected ? "primary.500" : "transparent"}
|
||||||
color={isSelected ? "white" : "fg"}
|
color={isSelected ? "white" : "fg"}
|
||||||
_hover={{ bg: isSelected ? "primary.600" : hoverBg }}
|
_hover={{
|
||||||
|
bg: isSelected ? "primary.600" : hoverBg,
|
||||||
|
}}
|
||||||
onClick={() => setSelectedCountryId(country.id)}
|
onClick={() => setSelectedCountryId(country.id)}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
{country.flag ? (
|
{country.flag ? (
|
||||||
<img src={country.flag} width="20" height="20" style={{ borderRadius: "50%", objectFit: "cover" }} alt={country.name} />
|
<img
|
||||||
) : <LuMapPin />}
|
src={country.flag}
|
||||||
<Text fontWeight={isSelected ? "bold" : "medium"}>{country.name}</Text>
|
width="20"
|
||||||
|
height="20"
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
alt={country.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LuMapPin />
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
fontWeight={
|
||||||
|
isSelected ? "bold" : "medium"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{country.name}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge size="sm" bg={isSelected ? "whiteAlpha.300" : "gray.100"} color={isSelected ? "white" : "fg"}>
|
<Badge
|
||||||
{leagues.data?.data?.filter(l => l.countryId === country.id).length || 0}
|
size="sm"
|
||||||
|
bg={
|
||||||
|
isSelected ? "whiteAlpha.300" : "gray.100"
|
||||||
|
}
|
||||||
|
color={isSelected ? "white" : "fg"}
|
||||||
|
>
|
||||||
|
{leagues.data?.data?.filter(
|
||||||
|
(l) => l.countryId === country.id,
|
||||||
|
).length || 0}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -231,44 +387,99 @@ export default function LeaguesContent() {
|
|||||||
{/* Right Area: Leagues Grid */}
|
{/* Right Area: Leagues Grid */}
|
||||||
<Box flex={1} p={{ base: 4, md: 8 }} bg={cardBg}>
|
<Box flex={1} p={{ base: 4, md: 8 }} bg={cardBg}>
|
||||||
{/* Top Filters */}
|
{/* Top Filters */}
|
||||||
<Flex justify="space-between" align="center" mb={6} direction={{ base: "column", sm: "row" }} gap={4}>
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
mb={6}
|
||||||
|
direction={{ base: "column", sm: "row" }}
|
||||||
|
gap={4}
|
||||||
|
>
|
||||||
<Heading size="md" fontWeight="bold">
|
<Heading size="md" fontWeight="bold">
|
||||||
{selectedCountryId
|
{selectedCountryId
|
||||||
? `${countries.data?.data?.find(c => c.id === selectedCountryId)?.name} ${t("leagues")}`
|
? `${countries.data?.data?.find((c) => c.id === selectedCountryId)?.name} ${t("leagues")}`
|
||||||
: t("leagues")}
|
: t("leagues")}
|
||||||
<Text as="span" color="fg.muted" ml={2} fontWeight="normal" fontSize="sm">
|
<Text
|
||||||
|
as="span"
|
||||||
|
color="fg.muted"
|
||||||
|
ml={2}
|
||||||
|
fontWeight="normal"
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
({displayedLeagues.length})
|
({displayedLeagues.length})
|
||||||
</Text>
|
</Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<HStack gap={2} bg={useColorModeValue("gray.100", "gray.800")} p={1} borderRadius="full">
|
<HStack
|
||||||
|
gap={2}
|
||||||
|
bg={useColorModeValue("gray.100", "gray.800")}
|
||||||
|
p={1}
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
px={4}
|
||||||
|
py={1.5}
|
||||||
|
borderRadius="full"
|
||||||
|
cursor="pointer"
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="medium"
|
||||||
bg={!sportFilter ? "white" : "transparent"}
|
bg={!sportFilter ? "white" : "transparent"}
|
||||||
color={!sportFilter ? "black" : "fg.muted"}
|
color={!sportFilter ? "black" : "fg.muted"}
|
||||||
shadow={!sportFilter ? "sm" : "none"}
|
shadow={!sportFilter ? "sm" : "none"}
|
||||||
onClick={() => setSportFilter("")}
|
onClick={() => setSportFilter("")}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
_dark={{ bg: !sportFilter ? "gray.600" : "transparent", color: !sportFilter ? "white" : "gray.400" }}
|
_dark={{
|
||||||
|
bg: !sportFilter ? "gray.600" : "transparent",
|
||||||
|
color: !sportFilter ? "white" : "gray.400",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t("all")}
|
{t("all")}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
px={4}
|
||||||
bg={sportFilter === "football" ? "green.500" : "transparent"}
|
py={1.5}
|
||||||
color={sportFilter === "football" ? "white" : "fg.muted"}
|
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"}
|
shadow={sportFilter === "football" ? "sm" : "none"}
|
||||||
onClick={() => setSportFilter(sportFilter === "football" ? "" : "football")}
|
onClick={() =>
|
||||||
|
setSportFilter(
|
||||||
|
sportFilter === "football" ? "" : "football",
|
||||||
|
)
|
||||||
|
}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
{tMatches("football")}
|
{tMatches("football")}
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
|
px={4}
|
||||||
bg={sportFilter === "basketball" ? "orange.500" : "transparent"}
|
py={1.5}
|
||||||
color={sportFilter === "basketball" ? "white" : "fg.muted"}
|
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"}
|
shadow={sportFilter === "basketball" ? "sm" : "none"}
|
||||||
onClick={() => setSportFilter(sportFilter === "basketball" ? "" : "basketball")}
|
onClick={() =>
|
||||||
|
setSportFilter(
|
||||||
|
sportFilter === "basketball" ? "" : "basketball",
|
||||||
|
)
|
||||||
|
}
|
||||||
transition="all 0.2s"
|
transition="all 0.2s"
|
||||||
>
|
>
|
||||||
{tMatches("basketball")}
|
{tMatches("basketball")}
|
||||||
@@ -278,17 +489,46 @@ export default function LeaguesContent() {
|
|||||||
|
|
||||||
{/* Leagues Grid */}
|
{/* Leagues Grid */}
|
||||||
{leagues.isLoading ? (
|
{leagues.isLoading ? (
|
||||||
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
|
<Flex justify="center" py={20}>
|
||||||
|
<Spinner
|
||||||
|
size="xl"
|
||||||
|
color="primary.500"
|
||||||
|
borderWidth="3px"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
) : displayedLeagues.length === 0 ? (
|
) : displayedLeagues.length === 0 ? (
|
||||||
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
<Flex
|
||||||
<Box bg="gray.100" _dark={{ bg: "gray.800" }} p={6} borderRadius="full" mb={4}>
|
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" />
|
<LuTrophy size={40} color="gray" />
|
||||||
</Box>
|
</Box>
|
||||||
<Heading size="md" mb={2}>Bulunamadı</Heading>
|
<Heading size="md" mb={2}>
|
||||||
<Text color="fg.muted">Seçili kriterlere uygun lig bulunamadı.</Text>
|
Bulunamadı
|
||||||
|
</Heading>
|
||||||
|
<Text color="fg.muted">
|
||||||
|
Seçili kriterlere uygun lig bulunamadı.
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
|
<Grid
|
||||||
|
templateColumns={{
|
||||||
|
base: "1fr",
|
||||||
|
md: "repeat(2, 1fr)",
|
||||||
|
xl: "repeat(3, 1fr)",
|
||||||
|
}}
|
||||||
|
gap={4}
|
||||||
|
>
|
||||||
{displayedLeagues.map((league: LeagueDto) => (
|
{displayedLeagues.map((league: LeagueDto) => (
|
||||||
<GridItem key={league.id}>
|
<GridItem key={league.id}>
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
@@ -311,27 +551,88 @@ export default function LeaguesContent() {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
data-group
|
data-group
|
||||||
>
|
>
|
||||||
<Flex justify="space-between" align="flex-start" mb={4}>
|
<Flex
|
||||||
<Box p={2} borderRadius="lg" bg={league.sport === "football" ? "green.50" : "orange.50"} _dark={{ bg: league.sport === "football" ? "green.900" : "orange.900" }}>
|
justify="space-between"
|
||||||
<LuTrophy size={20} color={league.sport === "football" ? "var(--chakra-colors-green-500)" : "var(--chakra-colors-orange-500)"} />
|
align="flex-start"
|
||||||
|
mb={4}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg={
|
||||||
|
league.sport === "football"
|
||||||
|
? "green.50"
|
||||||
|
: "orange.50"
|
||||||
|
}
|
||||||
|
_dark={{
|
||||||
|
bg:
|
||||||
|
league.sport === "football"
|
||||||
|
? "green.900"
|
||||||
|
: "orange.900",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuTrophy
|
||||||
|
size={20}
|
||||||
|
color={
|
||||||
|
league.sport === "football"
|
||||||
|
? "var(--chakra-colors-green-500)"
|
||||||
|
: "var(--chakra-colors-orange-500)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Badge size="sm" variant="subtle" colorScheme={league.sport === "football" ? "green" : "orange"}>
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
colorScheme={
|
||||||
|
league.sport === "football"
|
||||||
|
? "green"
|
||||||
|
: "orange"
|
||||||
|
}
|
||||||
|
>
|
||||||
{league.sport}
|
{league.sport}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Heading size="sm" mb={1} lineClamp={1} _groupHover={{ color: "primary.500" }}>
|
<Heading
|
||||||
|
size="sm"
|
||||||
|
mb={1}
|
||||||
|
lineClamp={1}
|
||||||
|
_groupHover={{ color: "primary.500" }}
|
||||||
|
>
|
||||||
{league.name}
|
{league.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
<HStack color="fg.muted" fontSize="sm" gap={1}>
|
<HStack color="fg.muted" fontSize="sm" gap={1}>
|
||||||
<LuMapPin size={14} />
|
<LuMapPin size={14} />
|
||||||
<Text lineClamp={1}>{league.country?.name || "Global"}</Text>
|
<Text lineClamp={1}>
|
||||||
|
{league.country?.name || "Global"}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{league.season && (
|
{league.season && (
|
||||||
<Flex mt={4} pt={4} borderTopWidth="1px" borderColor={borderColor} justify="space-between" align="center">
|
<Flex
|
||||||
<Text fontSize="xs" color="fg.muted" fontWeight="medium">SEZON: {league.season}</Text>
|
mt={4}
|
||||||
<Icon as={LuArrowRight} color="gray.400" _groupHover={{ color: "primary.500", transform: "translateX(4px)" }} transition="all 0.2s" />
|
pt={4}
|
||||||
|
borderTopWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color="fg.muted"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
SEZON: {league.season}
|
||||||
|
</Text>
|
||||||
|
<Icon
|
||||||
|
as={LuArrowRight}
|
||||||
|
color="gray.400"
|
||||||
|
_groupHover={{
|
||||||
|
color: "primary.500",
|
||||||
|
transform: "translateX(4px)",
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
@@ -347,7 +648,10 @@ export default function LeaguesContent() {
|
|||||||
{activeTab === "teams" && (
|
{activeTab === "teams" && (
|
||||||
<Box p={{ base: 4, md: 8 }}>
|
<Box p={{ base: 4, md: 8 }}>
|
||||||
<Box maxW="2xl" mx="auto" mb={10}>
|
<Box maxW="2xl" mx="auto" mb={10}>
|
||||||
<InputGroup startElement={<LuSearch color="gray.400" size={20} />} w="full">
|
<InputGroup
|
||||||
|
startElement={<LuSearch color="gray.400" size={20} />}
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder={tMatches("search-teams") + "..."}
|
placeholder={tMatches("search-teams") + "..."}
|
||||||
value={teamSearchQuery}
|
value={teamSearchQuery}
|
||||||
@@ -357,30 +661,70 @@ export default function LeaguesContent() {
|
|||||||
fontSize="lg"
|
fontSize="lg"
|
||||||
py={6}
|
py={6}
|
||||||
boxShadow="sm"
|
boxShadow="sm"
|
||||||
_focus={{ boxShadow: "0 0 0 2px var(--chakra-colors-primary-500)" }}
|
_focus={{
|
||||||
|
boxShadow: "0 0 0 2px var(--chakra-colors-primary-500)",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{debouncedTeamQuery.length < 2 ? (
|
{debouncedTeamQuery.length < 2 ? (
|
||||||
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
<Flex
|
||||||
<Box bg="primary.50" _dark={{ bg: "primary.900" }} p={8} borderRadius="full" mb={6}>
|
direction="column"
|
||||||
<LuUsers size={64} color="var(--chakra-colors-primary-500)" />
|
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>
|
</Box>
|
||||||
<Heading size="lg" mb={3}>{t("search-at-least-2")}</Heading>
|
<Heading size="lg" mb={3}>
|
||||||
|
{t("search-at-least-2")}
|
||||||
|
</Heading>
|
||||||
<Text color="fg.muted" maxW="md">
|
<Text color="fg.muted" maxW="md">
|
||||||
Find detailed statistics, upcoming matches, and head-to-head analysis by searching for any team worldwide.
|
Find detailed statistics, upcoming matches, and
|
||||||
|
head-to-head analysis by searching for any team worldwide.
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : searchTeams.isLoading ? (
|
) : searchTeams.isLoading ? (
|
||||||
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
|
<Flex justify="center" py={20}>
|
||||||
|
<Spinner size="xl" color="primary.500" borderWidth="3px" />
|
||||||
|
</Flex>
|
||||||
) : searchTeams.data?.data?.length === 0 ? (
|
) : searchTeams.data?.data?.length === 0 ? (
|
||||||
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
|
<Flex
|
||||||
<Heading size="md" mb={2}>Takım Bulunamadı</Heading>
|
direction="column"
|
||||||
<Text color="fg.muted">"{debouncedTeamQuery}" aramasıyla eşleşen bir takım bulunamadı.</Text>
|
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>
|
||||||
) : (
|
) : (
|
||||||
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
|
<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) => (
|
||||||
<GridItem key={team.id}>
|
<GridItem key={team.id}>
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
@@ -404,24 +748,65 @@ export default function LeaguesContent() {
|
|||||||
data-group
|
data-group
|
||||||
>
|
>
|
||||||
{team.logo ? (
|
{team.logo ? (
|
||||||
<Box w={12} h={12} borderRadius="full" overflow="hidden" flexShrink={0} mr={4} bg="white" p={1} shadow="sm">
|
<Box
|
||||||
<img src={team.logo} width="100%" height="100%" style={{ objectFit: "contain" }} alt={team.name} />
|
w={12}
|
||||||
|
h={12}
|
||||||
|
borderRadius="full"
|
||||||
|
overflow="hidden"
|
||||||
|
flexShrink={0}
|
||||||
|
mr={4}
|
||||||
|
bg="white"
|
||||||
|
p={1}
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={team.logo}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
alt={team.name}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Flex w={12} h={12} borderRadius="full" bg="gray.100" _dark={{ bg: "gray.700" }} align="center" justify="center" flexShrink={0} mr={4}>
|
<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" />
|
<LuUsers size={20} color="gray" />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<VStack align="start" gap={0} flex={1}>
|
<VStack align="start" gap={0} flex={1}>
|
||||||
<Heading size="sm" lineClamp={1} _groupHover={{ color: "primary.500" }}>{team.name}</Heading>
|
<Heading
|
||||||
|
size="sm"
|
||||||
|
lineClamp={1}
|
||||||
|
_groupHover={{ color: "primary.500" }}
|
||||||
|
>
|
||||||
|
{team.name}
|
||||||
|
</Heading>
|
||||||
<HStack color="fg.muted" fontSize="xs" gap={1}>
|
<HStack color="fg.muted" fontSize="xs" gap={1}>
|
||||||
<LuMapPin size={12} />
|
<LuMapPin size={12} />
|
||||||
<Text lineClamp={1}>{team.country || "Global"}</Text>
|
<Text lineClamp={1}>
|
||||||
|
{team.country || "Global"}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<Badge ml={2} size="sm" colorScheme={team.sport === "football" ? "green" : "orange"} variant="subtle">
|
<Badge
|
||||||
|
ml={2}
|
||||||
|
size="sm"
|
||||||
|
colorScheme={
|
||||||
|
team.sport === "football" ? "green" : "orange"
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{team.sport}
|
{team.sport}
|
||||||
</Badge>
|
</Badge>
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Box, Flex, Text, Badge, Image, ScrollArea } from "@chakra-ui/react";
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Image,
|
|
||||||
ScrollArea,
|
|
||||||
} 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";
|
||||||
import type { ActiveLeagueDto } from "@/lib/api/matches/types";
|
import type { ActiveLeagueDto } from "@/lib/api/matches/types";
|
||||||
@@ -68,7 +61,9 @@ export default function LeagueFilterBar({
|
|||||||
py={2}
|
py={2}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
borderWidth="1.5px"
|
borderWidth="1.5px"
|
||||||
borderColor={selectedLeagueId === null ? activeBorder : chipBorder}
|
borderColor={
|
||||||
|
selectedLeagueId === null ? activeBorder : chipBorder
|
||||||
|
}
|
||||||
bg={selectedLeagueId === null ? activeBg : chipBg}
|
bg={selectedLeagueId === null ? activeBg : chipBg}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
@@ -133,7 +128,12 @@ export default function LeagueFilterBar({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* League name + country */}
|
{/* League name + country */}
|
||||||
<Flex direction="column" align="flex-start" gap={0} lineHeight="1">
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align="flex-start"
|
||||||
|
gap={0}
|
||||||
|
lineHeight="1"
|
||||||
|
>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
fontWeight={isActive ? "bold" : "medium"}
|
fontWeight={isActive ? "bold" : "medium"}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import { LuUsers, LuUser, LuInfo, LuShieldCheck, LuClock } from "react-icons/lu";
|
import {
|
||||||
|
LuUsers,
|
||||||
|
LuUser,
|
||||||
|
LuInfo,
|
||||||
|
LuShieldCheck,
|
||||||
|
LuClock,
|
||||||
|
} from "react-icons/lu";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
||||||
|
|
||||||
@@ -79,10 +85,18 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
const meta = getLineupSourceMeta(source);
|
const meta = getLineupSourceMeta(source);
|
||||||
|
|
||||||
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
||||||
if (homeLineups.length === 0 && match.lineups?.home && match.lineups.home.length > 0) {
|
if (
|
||||||
|
homeLineups.length === 0 &&
|
||||||
|
match.lineups?.home &&
|
||||||
|
match.lineups.home.length > 0
|
||||||
|
) {
|
||||||
homeLineups = match.lineups.home.slice(0, 11);
|
homeLineups = match.lineups.home.slice(0, 11);
|
||||||
}
|
}
|
||||||
if (awayLineups.length === 0 && match.lineups?.away && match.lineups.away.length > 0) {
|
if (
|
||||||
|
awayLineups.length === 0 &&
|
||||||
|
match.lineups?.away &&
|
||||||
|
match.lineups.away.length > 0
|
||||||
|
) {
|
||||||
awayLineups = match.lineups.away.slice(0, 11);
|
awayLineups = match.lineups.away.slice(0, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +113,7 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
{meta.title}
|
{meta.title}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge
|
<Badge colorPalette={meta.badgeColor} variant="subtle">
|
||||||
colorPalette={meta.badgeColor}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{meta.badge}
|
{meta.badge}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -271,10 +282,15 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
<Text fontWeight="semibold" color="fg.muted">
|
<Text fontWeight="semibold" color="fg.muted">
|
||||||
Kadro Henüz Açıklanmadı
|
Kadro Henüz Açıklanmadı
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="sm" color="fg.subtle" textAlign="center" maxW="sm">
|
<Text
|
||||||
{match.homeTeamName} ve {match.awayTeamName} kadroları maç saatine
|
fontSize="sm"
|
||||||
yakın güncellenecektir. AI analizi, takım istatistikleri ve güç
|
color="fg.subtle"
|
||||||
dengesi üzerinden yapılmaktadır.
|
textAlign="center"
|
||||||
|
maxW="sm"
|
||||||
|
>
|
||||||
|
{match.homeTeamName} ve {match.awayTeamName} kadroları maç
|
||||||
|
saatine yakın güncellenecektir. AI analizi, takım istatistikleri
|
||||||
|
ve güç dengesi üzerinden yapılmaktadır.
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -21,10 +21,20 @@ import { useColorModeValue } from "@/components/ui/color-mode";
|
|||||||
import { SlideUp, FadeIn } from "@/components/motion";
|
import { SlideUp, FadeIn } from "@/components/motion";
|
||||||
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
|
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
|
||||||
import { usePrediction } from "@/lib/api/predictions/use-hooks";
|
import { usePrediction } from "@/lib/api/predictions/use-hooks";
|
||||||
|
import { useGetMe } from "@/lib/api/users/use-hooks";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { UsersQueryKeys } from "@/lib/api/users/use-hooks";
|
||||||
import PredictionCard from "@/components/matches/prediction-card";
|
import PredictionCard from "@/components/matches/prediction-card";
|
||||||
import OddsCard from "@/components/matches/odds-card";
|
import OddsCard from "@/components/matches/odds-card";
|
||||||
import LineupsCard from "@/components/matches/lineups-card";
|
import LineupsCard from "@/components/matches/lineups-card";
|
||||||
import { LuArrowLeft, LuRefreshCw, LuShield, LuFlag, LuUser } from "react-icons/lu";
|
import {
|
||||||
|
LuArrowLeft,
|
||||||
|
LuRefreshCw,
|
||||||
|
LuShield,
|
||||||
|
LuFlag,
|
||||||
|
LuUser,
|
||||||
|
LuSparkles,
|
||||||
|
} from "react-icons/lu";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────
|
||||||
@@ -60,6 +70,10 @@ interface SidelinedData {
|
|||||||
export default function MatchDetailContent() {
|
export default function MatchDetailContent() {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
const tPred = useTranslations("predictions");
|
const tPred = useTranslations("predictions");
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { data: meData } = useGetMe();
|
||||||
|
const usageLimit = meData?.data?.usageLimit;
|
||||||
|
const hasLimit = usageLimit ? (usageLimit.maxAnalyses - usageLimit.analysisCount > 0) : true;
|
||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -70,9 +84,16 @@ export default function MatchDetailContent() {
|
|||||||
const {
|
const {
|
||||||
data: predictionData,
|
data: predictionData,
|
||||||
isLoading: predLoading,
|
isLoading: predLoading,
|
||||||
refetch: refetchPrediction,
|
refetch: refetchPredictionRaw,
|
||||||
|
isFetching: isPredFetching,
|
||||||
} = usePrediction(matchId);
|
} = usePrediction(matchId);
|
||||||
|
|
||||||
|
const refetchPrediction = async () => {
|
||||||
|
await refetchPredictionRaw();
|
||||||
|
// After refetching, update the limits in the header
|
||||||
|
queryClient.invalidateQueries({ queryKey: UsersQueryKeys.me() });
|
||||||
|
};
|
||||||
|
|
||||||
const headerBg = useColorModeValue("white", "gray.800");
|
const headerBg = useColorModeValue("white", "gray.800");
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
@@ -139,7 +160,13 @@ export default function MatchDetailContent() {
|
|||||||
>
|
>
|
||||||
{/* League Banner */}
|
{/* League Banner */}
|
||||||
{match.league && (
|
{match.league && (
|
||||||
<Box bg={subtleBg} px={4} py={2.5} borderBottomWidth="1px" borderColor={borderColor}>
|
<Box
|
||||||
|
bg={subtleBg}
|
||||||
|
px={4}
|
||||||
|
py={2.5}
|
||||||
|
borderBottomWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
>
|
||||||
<Flex justify="center" align="center" gap={2}>
|
<Flex justify="center" align="center" gap={2}>
|
||||||
{match.league.country?.flag && (
|
{match.league.country?.flag && (
|
||||||
<Image
|
<Image
|
||||||
@@ -151,7 +178,8 @@ export default function MatchDetailContent() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
||||||
{match.league.country?.name && `${match.league.country.name} • `}
|
{match.league.country?.name &&
|
||||||
|
`${match.league.country.name} • `}
|
||||||
{match.league.name}
|
{match.league.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -300,7 +328,10 @@ export default function MatchDetailContent() {
|
|||||||
>
|
>
|
||||||
<LuUser size={14} />
|
<LuUser size={14} />
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
{t("referee")}: <Text as="span" fontWeight="semibold" color="fg">{match.refereeName}</Text>
|
{t("referee")}:{" "}
|
||||||
|
<Text as="span" fontWeight="semibold" color="fg">
|
||||||
|
{match.refereeName}
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -312,7 +343,12 @@ export default function MatchDetailContent() {
|
|||||||
{/* ═══════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════ */}
|
||||||
{hasSidelined && (
|
{hasSidelined && (
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
mb={6}
|
||||||
|
>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Heading as="h2" size="md" mb={4}>
|
<Heading as="h2" size="md" mb={4}>
|
||||||
🏥 {t("sidelined")}
|
🏥 {t("sidelined")}
|
||||||
@@ -361,6 +397,7 @@ export default function MatchDetailContent() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => refetchPrediction()}
|
onClick={() => refetchPrediction()}
|
||||||
|
disabled={!hasLimit}
|
||||||
gap={1.5}
|
gap={1.5}
|
||||||
>
|
>
|
||||||
<LuRefreshCw />
|
<LuRefreshCw />
|
||||||
@@ -368,7 +405,7 @@ export default function MatchDetailContent() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{predLoading ? (
|
{predLoading || isPredFetching ? (
|
||||||
<Flex justify="center" py={10}>
|
<Flex justify="center" py={10}>
|
||||||
<Spinner size="md" color="primary.500" />
|
<Spinner size="md" color="primary.500" />
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -377,8 +414,21 @@ export default function MatchDetailContent() {
|
|||||||
) : (
|
) : (
|
||||||
<Card.Root borderColor={borderColor} borderRadius="xl">
|
<Card.Root borderColor={borderColor} borderRadius="xl">
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<Flex justify="center" align="center" py={8}>
|
<Flex direction="column" justify="center" align="center" py={8} gap={4}>
|
||||||
<Text color="fg.muted">{tPred("no-predictions")}</Text>
|
<Text color="fg.muted">{tPred("no-predictions", { defaultValue: "Tahmin bulunmuyor." })}</Text>
|
||||||
|
<Button
|
||||||
|
colorPalette="primary"
|
||||||
|
onClick={() => refetchPrediction()}
|
||||||
|
disabled={!hasLimit}
|
||||||
|
loading={isPredFetching}
|
||||||
|
>
|
||||||
|
<LuSparkles /> {tPred("generate", { defaultValue: "Yapay Zeka ile Analiz Et" })}
|
||||||
|
</Button>
|
||||||
|
{!hasLimit && (
|
||||||
|
<Text fontSize="sm" color="red.500">
|
||||||
|
{tCommon("limits.out_of_analysis", { defaultValue: "Günlük analiz limitiniz doldu." })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
@@ -409,15 +459,31 @@ interface SidelinedColumnProps {
|
|||||||
t: ReturnType<typeof useTranslations>;
|
t: ReturnType<typeof useTranslations>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }: SidelinedColumnProps) {
|
function SidelinedColumn({
|
||||||
|
team,
|
||||||
|
teamName,
|
||||||
|
teamLogo,
|
||||||
|
injuryBg,
|
||||||
|
injuryBorder,
|
||||||
|
t,
|
||||||
|
}: SidelinedColumnProps) {
|
||||||
const players = team?.players || [];
|
const players = team?.players || [];
|
||||||
|
|
||||||
if (players.length === 0) {
|
if (players.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HStack gap={2} mb={3}>
|
<HStack gap={2} mb={3}>
|
||||||
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
|
{teamLogo && (
|
||||||
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
|
<Image
|
||||||
|
src={teamLogo}
|
||||||
|
alt={teamName}
|
||||||
|
boxSize="20px"
|
||||||
|
objectFit="contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text fontSize="sm" fontWeight="bold">
|
||||||
|
{teamName}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="xs" color="fg.muted" fontStyle="italic">
|
<Text fontSize="xs" color="fg.muted" fontStyle="italic">
|
||||||
{t("no-sidelined")}
|
{t("no-sidelined")}
|
||||||
@@ -429,9 +495,23 @@ function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<HStack gap={2} mb={3}>
|
<HStack gap={2} mb={3}>
|
||||||
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
|
{teamLogo && (
|
||||||
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
|
<Image
|
||||||
<Badge colorPalette="red" variant="subtle" fontSize="2xs" borderRadius="full">
|
src={teamLogo}
|
||||||
|
alt={teamName}
|
||||||
|
boxSize="20px"
|
||||||
|
objectFit="contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text fontSize="sm" fontWeight="bold">
|
||||||
|
{teamName}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
colorPalette="red"
|
||||||
|
variant="subtle"
|
||||||
|
fontSize="2xs"
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
{players.length}
|
{players.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -458,17 +538,17 @@ function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
{player.description || (
|
{player.description ||
|
||||||
player.type === "injury"
|
(player.type === "injury"
|
||||||
? t("injury")
|
? t("injury")
|
||||||
: player.type === "suspended"
|
: player.type === "suspended"
|
||||||
? t("suspended")
|
? t("suspended")
|
||||||
: t("other-reason")
|
: t("other-reason"))}
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
{player.matchesMissed !== undefined && player.matchesMissed > 0 && (
|
{player.matchesMissed !== undefined &&
|
||||||
|
player.matchesMissed > 0 && (
|
||||||
<Badge colorPalette="red" variant="subtle" fontSize="2xs">
|
<Badge colorPalette="red" variant="subtle" fontSize="2xs">
|
||||||
{player.matchesMissed} {t("matches-missed")}
|
{player.matchesMissed} {t("matches-missed")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
import { Box, Grid, Text, Flex, Image, HStack, VStack } from "@chakra-ui/react";
|
import { Box, Grid, Text, Flex, Image, HStack, VStack } 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";
|
||||||
import { StaggerContainer, StaggerItem, ScrollSlideUp } from "@/components/motion";
|
import {
|
||||||
|
StaggerContainer,
|
||||||
|
StaggerItem,
|
||||||
|
ScrollSlideUp,
|
||||||
|
} from "@/components/motion";
|
||||||
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
||||||
import MatchCard from "./match-card";
|
import MatchCard from "./match-card";
|
||||||
import type {
|
import type {
|
||||||
@@ -53,7 +57,13 @@ function MatchCardSkeleton() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* League */}
|
{/* League */}
|
||||||
<Flex mt={3} pt={2} borderTopWidth="1px" borderColor={border} justify="center">
|
<Flex
|
||||||
|
mt={3}
|
||||||
|
pt={2}
|
||||||
|
borderTopWidth="1px"
|
||||||
|
borderColor={border}
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
<Skeleton height="12px" width="120px" />
|
<Skeleton height="12px" width="120px" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { Box, Flex, Heading, Group, Button } from "@chakra-ui/react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { SlideUp } from "@/components/motion";
|
import { SlideUp } from "@/components/motion";
|
||||||
import { SportFilter, LeagueSidebar, LeagueFilterBar, MatchList } from "@/components/matches";
|
import {
|
||||||
|
SportFilter,
|
||||||
|
LeagueSidebar,
|
||||||
|
LeagueFilterBar,
|
||||||
|
MatchList,
|
||||||
|
} from "@/components/matches";
|
||||||
import { useQueryMatches, useActiveLeagues } from "@/lib/api/matches/use-hooks";
|
import { useQueryMatches, useActiveLeagues } from "@/lib/api/matches/use-hooks";
|
||||||
import { useMatchStore } from "@/lib/stores/match-store";
|
import { useMatchStore } from "@/lib/stores/match-store";
|
||||||
|
|
||||||
@@ -37,7 +42,12 @@ export default function MatchesContent() {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const triggerQuery = (currentSport: typeof sport, currentLeague: string | null, currentFilter: QuickFilter, currentDate?: string) => {
|
const triggerQuery = (
|
||||||
|
currentSport: typeof sport,
|
||||||
|
currentLeague: string | null,
|
||||||
|
currentFilter: QuickFilter,
|
||||||
|
currentDate?: string,
|
||||||
|
) => {
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
sport: currentSport,
|
sport: currentSport,
|
||||||
leagueId: currentLeague || undefined,
|
leagueId: currentLeague || undefined,
|
||||||
@@ -107,7 +117,14 @@ export default function MatchesContent() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Quick Filters */}
|
{/* Quick Filters */}
|
||||||
<Flex mb={6} overflowX="auto" pb={2} css={{ "&::-webkit-scrollbar": { display: "none" } }} gap={4} align="center">
|
<Flex
|
||||||
|
mb={6}
|
||||||
|
overflowX="auto"
|
||||||
|
pb={2}
|
||||||
|
css={{ "&::-webkit-scrollbar": { display: "none" } }}
|
||||||
|
gap={4}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
<Group attached>
|
<Group attached>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -163,7 +180,7 @@ export default function MatchesContent() {
|
|||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
background: "transparent",
|
background: "transparent",
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
outline: "none"
|
outline: "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -52,30 +52,54 @@ function formatReasonFallback(reason: string): string {
|
|||||||
if (evMatch) return `Teorik avantaj sinyali: Not ${evMatch[2]}`;
|
if (evMatch) return `Teorik avantaj sinyali: Not ${evMatch[2]}`;
|
||||||
const negMatch = reason.match(/^negative_model_edge_([+\-][\d.]+)$/);
|
const negMatch = reason.match(/^negative_model_edge_([+\-][\d.]+)$/);
|
||||||
if (negMatch) return `Model avantajı negatif (${negMatch[1]})`;
|
if (negMatch) return `Model avantajı negatif (${negMatch[1]})`;
|
||||||
const thresholdMatch = reason.match(/^below_market_edge_threshold_([+\-]?[\d.]+)$/);
|
const thresholdMatch = reason.match(
|
||||||
if (thresholdMatch) return `Piyasa avantaj eşiğinin altında (${thresholdMatch[1]})`;
|
/^below_market_edge_threshold_([+\-]?[\d.]+)$/,
|
||||||
if (reason === "confidence_interval_too_wide") return "Güven aralığı fazla geniş.";
|
);
|
||||||
|
if (thresholdMatch)
|
||||||
|
return `Piyasa avantaj eşiğinin altında (${thresholdMatch[1]})`;
|
||||||
|
if (reason === "confidence_interval_too_wide")
|
||||||
|
return "Güven aralığı fazla geniş.";
|
||||||
if (reason === "confidence_band_low") return "Güven bandı düşük.";
|
if (reason === "confidence_band_low") return "Güven bandı düşük.";
|
||||||
if (reason === "draw_probability_elevated") return "Beraberlik olasılığı yükselmiş görünüyor.";
|
if (reason === "draw_probability_elevated")
|
||||||
if (reason === "balanced_match_risk") return "Maç dengeli görünüyor, sürpriz riski var.";
|
return "Beraberlik olasılığı yükselmiş görünüyor.";
|
||||||
if (reason === "high_total_goal_volatility") return "Yüksek gol temposu sürpriz riskini artırıyor.";
|
if (reason === "balanced_match_risk")
|
||||||
if (reason === "mutual_goal_pressure") return "İki takım da gol tehdidi üretiyor.";
|
return "Maç dengeli görünüyor, sürpriz riski var.";
|
||||||
if (reason === "late_goal_swing_risk") return "Geç gol veya skor kırılması riski yüksek.";
|
if (reason === "high_total_goal_volatility")
|
||||||
if (reason === "live_match_open_state") return "Canlı maç tamamen açık oyuna dönmüş durumda.";
|
return "Yüksek gol temposu sürpriz riskini artırıyor.";
|
||||||
if (reason === "live_match_active_state") return "Canlı maç beklenenden daha hareketli ilerliyor.";
|
if (reason === "mutual_goal_pressure")
|
||||||
if (reason === "live_state_impossible_market") return "Canlı maç durumu bu marketi geçersiz kılıyor.";
|
return "İki takım da gol tehdidi üretiyor.";
|
||||||
if (reason === "live_score_exceeds_under_line") return "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti.";
|
if (reason === "late_goal_swing_risk")
|
||||||
if (reason === "score_model_conflicts_with_under_pick") return "Skor ve xG modeli bu alt seçeneğiyle çelişiyor.";
|
return "Geç gol veya skor kırılması riski yüksek.";
|
||||||
if (reason === "score_model_conflicts_with_over_pick") return "Skor ve xG modeli bu üst seçeneğiyle çelişiyor.";
|
if (reason === "live_match_open_state")
|
||||||
if (reason === "market_stack_conflict_over25") return "2.5 üst sinyali bu marketle çelişiyor.";
|
return "Canlı maç tamamen açık oyuna dönmüş durumda.";
|
||||||
if (reason === "market_stack_conflict_btts") return "KG Var sinyali bu marketle çelişiyor.";
|
if (reason === "live_match_active_state")
|
||||||
if (reason === "live_total_goals_close_to_line") return "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı.";
|
return "Canlı maç beklenenden daha hareketli ilerliyor.";
|
||||||
if (reason === "score_model_conflicts_with_btts_no") return "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor.";
|
if (reason === "live_state_impossible_market")
|
||||||
if (reason === "score_model_conflicts_with_draw_pick") return "Skor modeli beraberlik seçeneğini desteklemiyor.";
|
return "Canlı maç durumu bu marketi geçersiz kılıyor.";
|
||||||
if (reason === "score_model_conflicts_with_home_pick") return "Skor modeli ev sahibi seçeneğini desteklemiyor.";
|
if (reason === "live_score_exceeds_under_line")
|
||||||
if (reason === "score_model_conflicts_with_away_pick") return "Skor modeli deplasman seçeneğini desteklemiyor.";
|
return "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti.";
|
||||||
|
if (reason === "score_model_conflicts_with_under_pick")
|
||||||
|
return "Skor ve xG modeli bu alt seçeneğiyle çelişiyor.";
|
||||||
|
if (reason === "score_model_conflicts_with_over_pick")
|
||||||
|
return "Skor ve xG modeli bu üst seçeneğiyle çelişiyor.";
|
||||||
|
if (reason === "market_stack_conflict_over25")
|
||||||
|
return "2.5 üst sinyali bu marketle çelişiyor.";
|
||||||
|
if (reason === "market_stack_conflict_btts")
|
||||||
|
return "KG Var sinyali bu marketle çelişiyor.";
|
||||||
|
if (reason === "live_total_goals_close_to_line")
|
||||||
|
return "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı.";
|
||||||
|
if (reason === "score_model_conflicts_with_btts_no")
|
||||||
|
return "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor.";
|
||||||
|
if (reason === "score_model_conflicts_with_draw_pick")
|
||||||
|
return "Skor modeli beraberlik seçeneğini desteklemiyor.";
|
||||||
|
if (reason === "score_model_conflicts_with_home_pick")
|
||||||
|
return "Skor modeli ev sahibi seçeneğini desteklemiyor.";
|
||||||
|
if (reason === "score_model_conflicts_with_away_pick")
|
||||||
|
return "Skor modeli deplasman seçeneğini desteklemiyor.";
|
||||||
if (/^[a-z0-9_]+$/i.test(reason)) {
|
if (/^[a-z0-9_]+$/i.test(reason)) {
|
||||||
return reason.replace(/_/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
return reason
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/^\w/, (char) => char.toUpperCase());
|
||||||
}
|
}
|
||||||
return reason;
|
return reason;
|
||||||
}
|
}
|
||||||
@@ -101,7 +125,8 @@ function formatEdgeSignal(value?: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEdgePalette(value?: number): string {
|
function getEdgePalette(value?: number): string {
|
||||||
if (value === undefined || value === null || Number.isNaN(value)) return "gray";
|
if (value === undefined || value === null || Number.isNaN(value))
|
||||||
|
return "gray";
|
||||||
if (value <= 0) return "red";
|
if (value <= 0) return "red";
|
||||||
if (value < 0.08) return "yellow";
|
if (value < 0.08) return "yellow";
|
||||||
if (value < 0.15) return "orange";
|
if (value < 0.15) return "orange";
|
||||||
@@ -211,7 +236,10 @@ function getMarketLabel(
|
|||||||
market: string,
|
market: string,
|
||||||
marketLabels?: Record<string, string>,
|
marketLabels?: Record<string, string>,
|
||||||
): string {
|
): string {
|
||||||
if (marketLabels && Object.prototype.hasOwnProperty.call(marketLabels, market)) {
|
if (
|
||||||
|
marketLabels &&
|
||||||
|
Object.prototype.hasOwnProperty.call(marketLabels, market)
|
||||||
|
) {
|
||||||
return marketLabels[market];
|
return marketLabels[market];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +306,13 @@ function getPredictionSport(prediction: MatchPredictionDto): SportType {
|
|||||||
return "football";
|
return "football";
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIGNAL_TIER_ORDER: SignalTier[] = ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"];
|
const SIGNAL_TIER_ORDER: SignalTier[] = [
|
||||||
|
"CORE",
|
||||||
|
"VALUE",
|
||||||
|
"LEAN",
|
||||||
|
"LONGSHOT",
|
||||||
|
"PASS",
|
||||||
|
];
|
||||||
|
|
||||||
function getSignalTierPalette(tier?: SignalTier) {
|
function getSignalTierPalette(tier?: SignalTier) {
|
||||||
switch (tier) {
|
switch (tier) {
|
||||||
@@ -318,7 +352,12 @@ function TooltipIcon({ content }: { content: string }) {
|
|||||||
positioning={{ placement: "top" }}
|
positioning={{ placement: "top" }}
|
||||||
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||||
>
|
>
|
||||||
<IconButton aria-label="Bilgi" variant="ghost" size="2xs" colorPalette="gray">
|
<IconButton
|
||||||
|
aria-label="Bilgi"
|
||||||
|
variant="ghost"
|
||||||
|
size="2xs"
|
||||||
|
colorPalette="gray"
|
||||||
|
>
|
||||||
<LuCircleHelp />
|
<LuCircleHelp />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -361,7 +400,13 @@ function MetricTile({
|
|||||||
const bg = useColorModeValue("gray.50", "whiteAlpha.50");
|
const bg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
return (
|
return (
|
||||||
<Box p={3.5} bg={bg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
<Box
|
||||||
|
p={3.5}
|
||||||
|
bg={bg}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
<HStack justify="space-between" mb={1.5}>
|
<HStack justify="space-between" mb={1.5}>
|
||||||
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
|
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
|
||||||
{label}
|
{label}
|
||||||
@@ -388,7 +433,12 @@ function Bar({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box h={height} w="full" bg={trackBg} borderRadius="full" overflow="hidden">
|
<Box h={height} w="full" bg={trackBg} borderRadius="full" overflow="hidden">
|
||||||
<Box h="full" w={`${Math.max(0, Math.min(100, value))}%`} bg={color} borderRadius="full" />
|
<Box
|
||||||
|
h="full"
|
||||||
|
w={`${Math.max(0, Math.min(100, value))}%`}
|
||||||
|
bg={color}
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -484,7 +534,12 @@ function PickCard({
|
|||||||
return (
|
return (
|
||||||
<Card.Root bg={bg} borderColor={borderColor} borderRadius="2xl">
|
<Card.Root bg={bg} borderColor={borderColor} borderRadius="2xl">
|
||||||
<Card.Body gap={4}>
|
<Card.Body gap={4}>
|
||||||
<Flex justify="space-between" align={{ base: "start", md: "center" }} direction={{ base: "column", md: "row" }} gap={3}>
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
align={{ base: "start", md: "center" }}
|
||||||
|
direction={{ base: "column", md: "row" }}
|
||||||
|
gap={3}
|
||||||
|
>
|
||||||
<VStack align="start" gap={2}>
|
<VStack align="start" gap={2}>
|
||||||
<Badge colorPalette={palette} variant="solid" borderRadius="full">
|
<Badge colorPalette={palette} variant="solid" borderRadius="full">
|
||||||
{title}
|
{title}
|
||||||
@@ -493,30 +548,50 @@ function PickCard({
|
|||||||
{pick.pick}
|
{pick.pick}
|
||||||
</Text>
|
</Text>
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
<Badge variant="subtle">{getMarketLabel(pick.market, marketLabels)}</Badge>
|
<Badge variant="subtle">
|
||||||
<Badge colorPalette={pick.playable ? "green" : "gray"} variant="subtle">
|
{getMarketLabel(pick.market, marketLabels)}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
colorPalette={pick.playable ? "green" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{pick.bet_grade}
|
{pick.bet_grade}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={getSignalTierPalette(pick.signal_tier)} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={getSignalTierPalette(pick.signal_tier)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{getSignalTierLabel(pick.signal_tier)}
|
{getSignalTierLabel(pick.signal_tier)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
||||||
{getConfidenceBandLabel(pick.confidence_interval?.band)}
|
{getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={getEdgePalette(pick.ev_edge)} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={getEdgePalette(pick.ev_edge)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
|
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
<SimpleGrid columns={2} gap={3} minW={{ base: "full", md: "320px" }}>
|
<SimpleGrid columns={2} gap={3} minW={{ base: "full", md: "320px" }}>
|
||||||
<MetricTile label={labels.confidence} value={formatPercent(pick.calibrated_confidence, 0)} />
|
<MetricTile
|
||||||
|
label={labels.confidence}
|
||||||
|
value={formatPercent(pick.calibrated_confidence, 0)}
|
||||||
|
/>
|
||||||
<MetricTile label={labels.odds} value={formatOdds(pick.odds)} />
|
<MetricTile label={labels.odds} value={formatOdds(pick.odds)} />
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={labels.recommendedStake}
|
label={labels.recommendedStake}
|
||||||
value={formatUnits(pick.stake_units || stakeFallback)}
|
value={formatUnits(pick.stake_units || stakeFallback)}
|
||||||
/>
|
/>
|
||||||
<MetricTile label={labels.playScore} value={formatSignalScore(pick.play_score)} />
|
<MetricTile
|
||||||
<MetricTile label="Guven Araligi" value={formatInterval(pick.confidence_interval)} />
|
label={labels.playScore}
|
||||||
|
value={formatSignalScore(pick.play_score)}
|
||||||
|
/>
|
||||||
|
<MetricTile
|
||||||
|
label="Guven Araligi"
|
||||||
|
value={formatInterval(pick.confidence_interval)}
|
||||||
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Band"
|
label="Band"
|
||||||
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
|
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||||
@@ -524,7 +599,10 @@ function PickCard({
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ProbabilitySplit modelProb={pick.probability} impliedProb={pick.implied_prob} />
|
<ProbabilitySplit
|
||||||
|
modelProb={pick.probability}
|
||||||
|
impliedProb={pick.implied_prob}
|
||||||
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<HStack justify="space-between" mb={1.5}>
|
<HStack justify="space-between" mb={1.5}>
|
||||||
<Text fontSize="sm" fontWeight="semibold">
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
@@ -540,7 +618,10 @@ function PickCard({
|
|||||||
trackBg={trackBg}
|
trackBg={trackBg}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<ReasonList items={pick.decision_reasons} resolveReason={resolveReason} />
|
<ReasonList
|
||||||
|
items={pick.decision_reasons}
|
||||||
|
resolveReason={resolveReason}
|
||||||
|
/>
|
||||||
{pick.confidence_interval && !pick.confidence_interval.threshold_met ? (
|
{pick.confidence_interval && !pick.confidence_interval.threshold_met ? (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
@@ -550,7 +631,8 @@ function PickCard({
|
|||||||
borderColor={intervalWarningBorder}
|
borderColor={intervalWarningBorder}
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi onerilmez.
|
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
|
||||||
|
onerilmez.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -582,8 +664,12 @@ function SummaryTable({
|
|||||||
{items
|
{items
|
||||||
.slice()
|
.slice()
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const leftIndex = SIGNAL_TIER_ORDER.indexOf(left.signal_tier || "PASS");
|
const leftIndex = SIGNAL_TIER_ORDER.indexOf(
|
||||||
const rightIndex = SIGNAL_TIER_ORDER.indexOf(right.signal_tier || "PASS");
|
left.signal_tier || "PASS",
|
||||||
|
);
|
||||||
|
const rightIndex = SIGNAL_TIER_ORDER.indexOf(
|
||||||
|
right.signal_tier || "PASS",
|
||||||
|
);
|
||||||
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
||||||
return right.calibrated_confidence - left.calibrated_confidence;
|
return right.calibrated_confidence - left.calibrated_confidence;
|
||||||
})
|
})
|
||||||
@@ -602,25 +688,46 @@ function SummaryTable({
|
|||||||
borderColor={item.playable ? "green.200" : borderColor}
|
borderColor={item.playable ? "green.200" : borderColor}
|
||||||
>
|
>
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
<Badge colorPalette={item.playable ? "green" : "gray"} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={item.playable ? "green" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{item.bet_grade}
|
{item.bet_grade}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={getSignalTierPalette(item.signal_tier)} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={getSignalTierPalette(item.signal_tier)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{getSignalTierLabel(item.signal_tier)}
|
{getSignalTierLabel(item.signal_tier)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fontWeight="semibold">{getMarketLabel(item.market, marketLabels)}</Text>
|
<Text fontWeight="semibold">
|
||||||
|
{getMarketLabel(item.market, marketLabels)}
|
||||||
|
</Text>
|
||||||
<Text color="fg.muted">{item.pick}</Text>
|
<Text color="fg.muted">{item.pick}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack gap={5} fontSize="sm">
|
<HStack gap={5} fontSize="sm">
|
||||||
<Text minW="48px">{formatOdds(item.odds)}</Text>
|
<Text minW="48px">{formatOdds(item.odds)}</Text>
|
||||||
<Text minW="96px" color={`${getEdgePalette(item.ev_edge)}.500`} fontWeight="semibold">
|
<Text
|
||||||
|
minW="96px"
|
||||||
|
color={`${getEdgePalette(item.ev_edge)}.500`}
|
||||||
|
fontWeight="semibold"
|
||||||
|
>
|
||||||
{formatEdgeSignal(item.ev_edge)}
|
{formatEdgeSignal(item.ev_edge)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text minW="48px">{formatPercent(item.calibrated_confidence, 0)}</Text>
|
<Text minW="48px">
|
||||||
<Badge colorPalette={getConfidenceBandPalette(item.confidence_interval?.band)} variant="subtle">
|
{formatPercent(item.calibrated_confidence, 0)}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
colorPalette={getConfidenceBandPalette(
|
||||||
|
item.confidence_interval?.band,
|
||||||
|
)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
<Badge variant="surface">
|
||||||
|
{formatUnits(item.stake_units)}
|
||||||
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
@@ -650,7 +757,9 @@ function MarketBoardSection({
|
|||||||
|
|
||||||
if (!marketBoard || !Object.keys(marketBoard).length) return null;
|
if (!marketBoard || !Object.keys(marketBoard).length) return null;
|
||||||
|
|
||||||
const summaryByMarket = new Map((betSummary || []).map((item) => [item.market, item]));
|
const summaryByMarket = new Map(
|
||||||
|
(betSummary || []).map((item) => [item.market, item]),
|
||||||
|
);
|
||||||
const orderedEntries = Object.entries(marketBoard).sort(([left], [right]) => {
|
const orderedEntries = Object.entries(marketBoard).sort(([left], [right]) => {
|
||||||
const leftIndex = MARKET_ORDER.indexOf(left);
|
const leftIndex = MARKET_ORDER.indexOf(left);
|
||||||
const rightIndex = MARKET_ORDER.indexOf(right);
|
const rightIndex = MARKET_ORDER.indexOf(right);
|
||||||
@@ -662,11 +771,7 @@ function MarketBoardSection({
|
|||||||
return (
|
return (
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
<Card.Body gap={4}>
|
<Card.Body gap={4}>
|
||||||
<SectionTitle
|
<SectionTitle icon={LuChartNoAxesCombined} title={title} info={info} />
|
||||||
icon={LuChartNoAxesCombined}
|
|
||||||
title={title}
|
|
||||||
info={info}
|
|
||||||
/>
|
|
||||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||||
{orderedEntries.map(([market, entry]) => {
|
{orderedEntries.map(([market, entry]) => {
|
||||||
if (!entry?.probs) return null;
|
if (!entry?.probs) return null;
|
||||||
@@ -690,21 +795,33 @@ function MarketBoardSection({
|
|||||||
</Text>
|
</Text>
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
{summary ? (
|
{summary ? (
|
||||||
<Badge colorPalette={summary.playable ? "green" : "gray"} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={summary.playable ? "green" : "gray"}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{summary.playable ? "Oynanabilir" : "Riskli"}
|
{summary.playable ? "Oynanabilir" : "Riskli"}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{summary?.signal_tier ? (
|
{summary?.signal_tier ? (
|
||||||
<Badge colorPalette={getSignalTierPalette(summary.signal_tier)} variant="subtle">
|
<Badge
|
||||||
|
colorPalette={getSignalTierPalette(
|
||||||
|
summary.signal_tier,
|
||||||
|
)}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
{getSignalTierLabel(summary.signal_tier)}
|
{getSignalTierLabel(summary.signal_tier)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{summary?.bet_grade ? <Badge variant="outline">{summary.bet_grade}</Badge> : null}
|
{summary?.bet_grade ? (
|
||||||
|
<Badge variant="outline">{summary.bet_grade}</Badge>
|
||||||
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
{entry.pick ? (
|
{entry.pick ? (
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={getConfidenceBandPalette(entry.confidence_band || interval?.band)}
|
colorPalette={getConfidenceBandPalette(
|
||||||
|
entry.confidence_band || interval?.band,
|
||||||
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
>
|
>
|
||||||
@@ -720,7 +837,11 @@ function MarketBoardSection({
|
|||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Kalibre Guven"
|
label="Kalibre Guven"
|
||||||
value={summary ? formatPercent(summary.calibrated_confidence, 0) : "-"}
|
value={
|
||||||
|
summary
|
||||||
|
? formatPercent(summary.calibrated_confidence, 0)
|
||||||
|
: "-"
|
||||||
|
}
|
||||||
accent={summary?.playable ? "green.500" : "orange.500"}
|
accent={summary?.playable ? "green.500" : "orange.500"}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
@@ -807,7 +928,14 @@ function ScoreCard({
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid columns={{ base: 2, md: 5 }} gap={2}>
|
<SimpleGrid columns={{ base: 2, md: 5 }} gap={2}>
|
||||||
{prediction.scenario_top5.map((scenario) => (
|
{prediction.scenario_top5.map((scenario) => (
|
||||||
<Box key={`${scenario.score}-${scenario.prob}`} p={3} bg={subBg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
<Box
|
||||||
|
key={`${scenario.score}-${scenario.prob}`}
|
||||||
|
p={3}
|
||||||
|
bg={subBg}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
<Text fontSize="lg" fontWeight="bold">
|
<Text fontSize="lg" fontWeight="bold">
|
||||||
{scenario.score}
|
{scenario.score}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -835,7 +963,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
const ui = messages.predictions?.ui;
|
const ui = messages.predictions?.ui;
|
||||||
const uiText = (key: string, fallback: string) => ui?.[key] || fallback;
|
const uiText = (key: string, fallback: string) => ui?.[key] || fallback;
|
||||||
const resolveReason = (reason: string) =>
|
const resolveReason = (reason: string) =>
|
||||||
getPredictionReasonText(reason, messages.predictions?.["prediction-reasons"]);
|
getPredictionReasonText(
|
||||||
|
reason,
|
||||||
|
messages.predictions?.["prediction-reasons"],
|
||||||
|
);
|
||||||
|
|
||||||
const pageBg = useColorModeValue("gray.50", "gray.900");
|
const pageBg = useColorModeValue("gray.50", "gray.900");
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
@@ -864,7 +995,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
value: prediction.engine_breakdown.player,
|
value: prediction.engine_breakdown.player,
|
||||||
color: "green.400",
|
color: "green.400",
|
||||||
},
|
},
|
||||||
{ key: "odds", icon: LuTrendingUp, label: "Oran Analizi", value: prediction.engine_breakdown.odds, color: "orange.400" },
|
{
|
||||||
|
key: "odds",
|
||||||
|
icon: LuTrendingUp,
|
||||||
|
label: "Oran Analizi",
|
||||||
|
value: prediction.engine_breakdown.odds,
|
||||||
|
color: "orange.400",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "referee",
|
key: "referee",
|
||||||
icon: LuShieldAlert,
|
icon: LuShieldAlert,
|
||||||
@@ -894,30 +1031,50 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
>
|
>
|
||||||
<HStack align="start" gap={2}>
|
<HStack align="start" gap={2}>
|
||||||
<Icon as={LuShieldAlert} boxSize={4.5} color="orange.500" mt={0.5} />
|
<Icon
|
||||||
|
as={LuShieldAlert}
|
||||||
|
boxSize={4.5}
|
||||||
|
color="orange.500"
|
||||||
|
mt={0.5}
|
||||||
|
/>
|
||||||
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
||||||
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.
|
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi
|
||||||
|
değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi
|
||||||
|
nedeniyle yanılabilir.
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{recommendedPick ? (
|
{recommendedPick ? (
|
||||||
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
||||||
<Box p={4} bg={useColorModeValue("green.50", "green.950")} borderWidth="1px" borderColor={useColorModeValue("green.200", "green.800")} borderRadius="2xl">
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={useColorModeValue("green.50", "green.950")}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={useColorModeValue("green.200", "green.800")}
|
||||||
|
borderRadius="2xl"
|
||||||
|
>
|
||||||
<HStack justify="space-between" align="start" mb={4}>
|
<HStack justify="space-between" align="start" mb={4}>
|
||||||
<VStack align="start" gap={2}>
|
<VStack align="start" gap={2}>
|
||||||
<Badge colorPalette="green" variant="solid" borderRadius="full">
|
<Badge
|
||||||
|
colorPalette="green"
|
||||||
|
variant="solid"
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
{uiText("main-recommendation", "Öne Çıkan Sinyal")}
|
{uiText("main-recommendation", "Öne Çıkan Sinyal")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fontSize="2xl" fontWeight="bold">
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
{recommendedPick.pick}
|
{recommendedPick.pick}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
{getMarketLabel(recommendedPick.market, marketLabels)} {uiText("best-market-copy", "marketinde en guclu secim.")}
|
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
|
||||||
|
{uiText("best-market-copy", "marketinde en guclu secim.")}
|
||||||
</Text>
|
</Text>
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
<Badge colorPalette={mainBandPalette} variant="subtle">
|
<Badge colorPalette={mainBandPalette} variant="subtle">
|
||||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
{getConfidenceBandLabel(
|
||||||
|
prediction.bet_advice.confidence_band,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{recommendedPick.confidence_interval ? (
|
{recommendedPick.confidence_interval ? (
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
@@ -929,9 +1086,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<Icon as={LuBadgeCheck} boxSize={8} color="green.500" />
|
<Icon as={LuBadgeCheck} boxSize={8} color="green.500" />
|
||||||
</HStack>
|
</HStack>
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
||||||
<MetricTile label={uiText("confidence-label", "Guven")} value={formatPercent(recommendedPick.calibrated_confidence, 0)} />
|
<MetricTile
|
||||||
<MetricTile label={uiText("odds-label", "Oran")} value={formatOdds(recommendedPick.odds)} />
|
label={uiText("confidence-label", "Guven")}
|
||||||
<MetricTile label="Guven Araligi" value={formatInterval(recommendedPick.confidence_interval)} />
|
value={formatPercent(
|
||||||
|
recommendedPick.calibrated_confidence,
|
||||||
|
0,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<MetricTile
|
||||||
|
label={uiText("odds-label", "Oran")}
|
||||||
|
value={formatOdds(recommendedPick.odds)}
|
||||||
|
/>
|
||||||
|
<MetricTile
|
||||||
|
label="Guven Araligi"
|
||||||
|
value={formatInterval(recommendedPick.confidence_interval)}
|
||||||
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={uiText("edge-label", "Teorik Avantaj")}
|
label={uiText("edge-label", "Teorik Avantaj")}
|
||||||
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
||||||
@@ -955,7 +1124,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box p={4} bg={cardBg} borderWidth="1px" borderColor={borderColor} borderRadius="2xl">
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={cardBg}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="2xl"
|
||||||
|
>
|
||||||
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
||||||
{uiText("quick-read", "Hizli yorum")}
|
{uiText("quick-read", "Hizli yorum")}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -985,12 +1160,18 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={uiText("lineup-source", "Lineup Kaynagi")}
|
label={uiText("lineup-source", "Lineup Kaynagi")}
|
||||||
value={getLineupSourceLabel(prediction.data_quality.lineup_source)}
|
value={getLineupSourceLabel(
|
||||||
|
prediction.data_quality.lineup_source,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<MetricTile
|
||||||
|
label={uiText("model-label", "Model")}
|
||||||
|
value={prediction.model_version}
|
||||||
/>
|
/>
|
||||||
<MetricTile label={uiText("model-label", "Model")} value={prediction.model_version} />
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{prediction.risk.is_surprise_risk || prediction.risk.warnings?.length ? (
|
{prediction.risk.is_surprise_risk ||
|
||||||
|
prediction.risk.warnings?.length ? (
|
||||||
<Box
|
<Box
|
||||||
p={4}
|
p={4}
|
||||||
bg={useColorModeValue("orange.50", "orange.950")}
|
bg={useColorModeValue("orange.50", "orange.950")}
|
||||||
@@ -999,7 +1180,12 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
borderRadius="2xl"
|
borderRadius="2xl"
|
||||||
>
|
>
|
||||||
<HStack align="start" gap={3}>
|
<HStack align="start" gap={3}>
|
||||||
<Icon as={LuTriangleAlert} boxSize={5} color="orange.500" mt={0.5} />
|
<Icon
|
||||||
|
as={LuTriangleAlert}
|
||||||
|
boxSize={5}
|
||||||
|
color="orange.500"
|
||||||
|
mt={0.5}
|
||||||
|
/>
|
||||||
<VStack align="start" gap={1.5}>
|
<VStack align="start" gap={1.5}>
|
||||||
<Text fontWeight="semibold">Risk Yorumu</Text>
|
<Text fontWeight="semibold">Risk Yorumu</Text>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
@@ -1009,8 +1195,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
: "Model bu maçta ekstra dikkat istiyor.")}
|
: "Model bu maçta ekstra dikkat istiyor.")}
|
||||||
</Text>
|
</Text>
|
||||||
{prediction.risk.surprise_score !== undefined ? (
|
{prediction.risk.surprise_score !== undefined ? (
|
||||||
<Text fontSize="sm" fontWeight="semibold" color="orange.600">
|
<Text
|
||||||
Sürpriz skoru: {formatPercent(prediction.risk.surprise_score, 0)}
|
fontSize="sm"
|
||||||
|
fontWeight="semibold"
|
||||||
|
color="orange.600"
|
||||||
|
>
|
||||||
|
Sürpriz skoru:{" "}
|
||||||
|
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<ReasonList
|
<ReasonList
|
||||||
@@ -1032,11 +1223,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuChartColumn}
|
icon={LuChartColumn}
|
||||||
title={t("engine-breakdown-title")}
|
title={t("engine-breakdown-title")}
|
||||||
info={uiText("engine-info", "Tahmini en cok hangi bilesenlerin etkiledigini gosterir.")}
|
info={uiText(
|
||||||
|
"engine-info",
|
||||||
|
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
||||||
{engineItems.map((item) => (
|
{engineItems.map((item) => (
|
||||||
<Box key={item.key} p={4} bg={useColorModeValue("gray.50", "whiteAlpha.50")} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
<Box
|
||||||
|
key={item.key}
|
||||||
|
p={4}
|
||||||
|
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
<HStack justify="space-between" mb={2}>
|
<HStack justify="space-between" mb={2}>
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={item.icon} boxSize={4} color={item.color} />
|
<Icon as={item.icon} boxSize={4} color={item.color} />
|
||||||
@@ -1048,7 +1249,11 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
+{item.value.toFixed(1)}
|
+{item.value.toFixed(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
|
<Bar
|
||||||
|
value={Math.min(item.value, 100)}
|
||||||
|
color={item.color}
|
||||||
|
trackBg={useColorModeValue("gray.100", "gray.700")}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -1079,14 +1284,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuFlame}
|
icon={LuFlame}
|
||||||
title={uiText("alternative-markets", "Alternatif Marketler")}
|
title={uiText("alternative-markets", "Alternatif Marketler")}
|
||||||
info={uiText("alternative-markets-info", "Ana tahmin disindaki secenekler.")}
|
info={uiText(
|
||||||
|
"alternative-markets-info",
|
||||||
|
"Ana tahmin disindaki secenekler.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||||
{prediction.supporting_picks.map((pick) => (
|
{prediction.supporting_picks.map((pick) => (
|
||||||
<PickCard
|
<PickCard
|
||||||
key={`${pick.market}-${pick.pick}`}
|
key={`${pick.market}-${pick.pick}`}
|
||||||
pick={pick}
|
pick={pick}
|
||||||
title={pick.playable ? uiText("alternative", "Alternatif") : uiText("pass-market", "PASS market")}
|
title={
|
||||||
|
pick.playable
|
||||||
|
? uiText("alternative", "Alternatif")
|
||||||
|
: uiText("pass-market", "PASS market")
|
||||||
|
}
|
||||||
resolveReason={resolveReason}
|
resolveReason={resolveReason}
|
||||||
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
@@ -1108,7 +1320,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
items={prediction.bet_summary || []}
|
items={prediction.bet_summary || []}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
title={uiText("all-markets-title", "Tum Marketler")}
|
title={uiText("all-markets-title", "Tum Marketler")}
|
||||||
info={uiText("all-markets-info", "Butun secenekleri tek tabloda karsilastir.")}
|
info={uiText(
|
||||||
|
"all-markets-info",
|
||||||
|
"Butun secenekleri tek tabloda karsilastir.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<ScoreCard prediction={prediction} sport={sport} />
|
<ScoreCard prediction={prediction} sport={sport} />
|
||||||
<MarketBoardSection
|
<MarketBoardSection
|
||||||
@@ -1116,7 +1331,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
betSummary={prediction.bet_summary || []}
|
betSummary={prediction.bet_summary || []}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
title={t("market-board")}
|
title={t("market-board")}
|
||||||
info={uiText("market-board-info", "Modelin her markette gordugu olasilik dagilimi.")}
|
info={uiText(
|
||||||
|
"market-board-info",
|
||||||
|
"Modelin her markette gordugu olasilik dagilimi.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{prediction.v27_engine ? (
|
{prediction.v27_engine ? (
|
||||||
@@ -1130,16 +1348,37 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
title={t("bet-advice")}
|
title={t("bet-advice")}
|
||||||
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
|
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
|
||||||
/>
|
/>
|
||||||
<HStack justify="space-between" align={{ base: "start", md: "center" }} flexDir={{ base: "column", md: "row" }} gap={3}>
|
<HStack
|
||||||
|
justify="space-between"
|
||||||
|
align={{ base: "start", md: "center" }}
|
||||||
|
flexDir={{ base: "column", md: "row" }}
|
||||||
|
gap={3}
|
||||||
|
>
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<Badge colorPalette={prediction.bet_advice.playable ? "green" : "red"} variant="solid" borderRadius="full" fontSize="sm" px={3} py={1}>
|
<Badge
|
||||||
|
colorPalette={prediction.bet_advice.playable ? "green" : "red"}
|
||||||
|
variant="solid"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="sm"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
|
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={mainBandPalette} variant="subtle" borderRadius="full" fontSize="sm" px={3} py={1}>
|
<Badge
|
||||||
|
colorPalette={mainBandPalette}
|
||||||
|
variant="subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="sm"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
>
|
||||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={getSignalTierPalette(prediction.bet_advice.signal_tier)}
|
colorPalette={getSignalTierPalette(
|
||||||
|
prediction.bet_advice.signal_tier,
|
||||||
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
@@ -1148,15 +1387,25 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
>
|
>
|
||||||
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
|
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text color="fg.muted">{resolveReason(prediction.bet_advice.reason)}</Text>
|
<Text color="fg.muted">
|
||||||
|
{resolveReason(prediction.bet_advice.reason)}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
||||||
{uiText("recommended-stake-inline", "Onerilen miktar")}: {formatUnits(prediction.bet_advice.suggested_stake_units)}
|
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
|
||||||
|
{formatUnits(prediction.bet_advice.suggested_stake_units)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Separator />
|
<Separator />
|
||||||
<SectionTitle icon={LuBrain} title={t("reasoning")} info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti." />
|
<SectionTitle
|
||||||
<ReasonList items={prediction.reasoning_factors} resolveReason={resolveReason} />
|
icon={LuBrain}
|
||||||
|
title={t("reasoning")}
|
||||||
|
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
|
||||||
|
/>
|
||||||
|
<ReasonList
|
||||||
|
items={prediction.reasoning_factors}
|
||||||
|
resolveReason={resolveReason}
|
||||||
|
/>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -153,7 +153,12 @@ function TripleValueCard({
|
|||||||
isValue ? "green.300" : "gray.200",
|
isValue ? "green.300" : "gray.200",
|
||||||
isValue ? "green.700" : "gray.700",
|
isValue ? "green.700" : "gray.700",
|
||||||
);
|
);
|
||||||
const edgeColor = entry.edge > 0.03 ? "green.500" : entry.edge < -0.03 ? "red.400" : "fg.muted";
|
const edgeColor =
|
||||||
|
entry.edge > 0.03
|
||||||
|
? "green.500"
|
||||||
|
: entry.edge < -0.03
|
||||||
|
? "red.400"
|
||||||
|
: "fg.muted";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -249,13 +254,7 @@ function ProgressBar({
|
|||||||
const trackBg = useColorModeValue("gray.100", "gray.700");
|
const trackBg = useColorModeValue("gray.100", "gray.700");
|
||||||
const w = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
const w = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box h="10px" w="full" bg={trackBg} borderRadius="full" overflow="hidden">
|
||||||
h="10px"
|
|
||||||
w="full"
|
|
||||||
bg={trackBg}
|
|
||||||
borderRadius="full"
|
|
||||||
overflow="hidden"
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
h="full"
|
h="full"
|
||||||
w={`${w}%`}
|
w={`${w}%`}
|
||||||
@@ -460,13 +459,28 @@ function HtftGrid({
|
|||||||
{/* Column headers */}
|
{/* Column headers */}
|
||||||
<Grid templateColumns="50px repeat(3, 1fr)" gap={1.5} mb={1.5}>
|
<Grid templateColumns="50px repeat(3, 1fr)" gap={1.5} mb={1.5}>
|
||||||
<Box />
|
<Box />
|
||||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
textAlign="center"
|
||||||
|
color="fg.muted"
|
||||||
|
>
|
||||||
MS 1
|
MS 1
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
textAlign="center"
|
||||||
|
color="fg.muted"
|
||||||
|
>
|
||||||
MS X
|
MS X
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
textAlign="center"
|
||||||
|
color="fg.muted"
|
||||||
|
>
|
||||||
MS 2
|
MS 2
|
||||||
</Text>
|
</Text>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -611,7 +625,12 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
|
|
||||||
{/* Engine version badge */}
|
{/* Engine version badge */}
|
||||||
<HStack>
|
<HStack>
|
||||||
<Badge colorPalette="purple" variant="subtle" borderRadius="full" fontSize="2xs">
|
<Badge
|
||||||
|
colorPalette="purple"
|
||||||
|
variant="subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="2xs"
|
||||||
|
>
|
||||||
{engine.version}
|
{engine.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
{engine.consensus && (
|
{engine.consensus && (
|
||||||
@@ -621,11 +640,18 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
>
|
>
|
||||||
{engine.consensus === "AGREE" ? "Motorlar Uyumlu" : "Motorlar Farklı"}
|
{engine.consensus === "AGREE"
|
||||||
|
? "Motorlar Uyumlu"
|
||||||
|
: "Motorlar Farklı"}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{valueHits.length > 0 && (
|
{valueHits.length > 0 && (
|
||||||
<Badge colorPalette="green" variant="outline" borderRadius="full" fontSize="2xs">
|
<Badge
|
||||||
|
colorPalette="green"
|
||||||
|
variant="outline"
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="2xs"
|
||||||
|
>
|
||||||
{valueHits.length} Değer Sinyali
|
{valueHits.length} Değer Sinyali
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -656,7 +682,10 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
{/* Cards + HTFT side by side on large screens */}
|
{/* Cards + HTFT side by side on large screens */}
|
||||||
{(hasCards || hasHtft) && (
|
{(hasCards || hasHtft) && (
|
||||||
<Grid
|
<Grid
|
||||||
templateColumns={{ base: "1fr", xl: hasCards && hasHtft ? "1fr 1fr" : "1fr" }}
|
templateColumns={{
|
||||||
|
base: "1fr",
|
||||||
|
xl: hasCards && hasHtft ? "1fr 1fr" : "1fr",
|
||||||
|
}}
|
||||||
gap={4}
|
gap={4}
|
||||||
>
|
>
|
||||||
{hasCards && <CardsSection cards={cards} />}
|
{hasCards && <CardsSection cards={cards} />}
|
||||||
|
|||||||
@@ -154,8 +154,9 @@ export default function PredictionsContent() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={
|
colorPalette={
|
||||||
riskColors[pred.risk?.level?.toUpperCase()] ||
|
riskColors[
|
||||||
"gray"
|
pred.risk?.level?.toUpperCase()
|
||||||
|
] || "gray"
|
||||||
}
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
SimpleGrid,
|
||||||
|
Spinner,
|
||||||
|
Card,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useSubscriptionPlans } from "@/lib/api/subscriptions/use-hooks";
|
||||||
|
import { usePaddleCheckout } from "@/lib/paddle";
|
||||||
|
import { PricingCard } from "@/components/subscription";
|
||||||
|
import { LoginModal } from "@/components/auth/login-modal";
|
||||||
|
import { SlideUp } from "@/components/motion";
|
||||||
|
import { SegmentedControl } from "@/components/ui/forms/segmented-control";
|
||||||
|
import type {
|
||||||
|
PlanInfo,
|
||||||
|
PlanType,
|
||||||
|
BillingInterval,
|
||||||
|
} from "@/lib/api/subscriptions/types";
|
||||||
|
import { LuMessageCircleQuestion, LuChevronDown } from "react-icons/lu";
|
||||||
|
|
||||||
|
/** Static fallback plans used when the backend is not reachable */
|
||||||
|
const FALLBACK_PLANS: readonly PlanInfo[] = [
|
||||||
|
{
|
||||||
|
id: "free" as PlanType,
|
||||||
|
name: "Free",
|
||||||
|
description: "",
|
||||||
|
monthlyPrice: 0,
|
||||||
|
yearlyPrice: 0,
|
||||||
|
currency: "TRY",
|
||||||
|
features: [],
|
||||||
|
limits: { maxAnalyses: 3, maxCoupons: 1 },
|
||||||
|
highlighted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plus" as PlanType,
|
||||||
|
name: "Plus",
|
||||||
|
description: "",
|
||||||
|
monthlyPrice: 149,
|
||||||
|
yearlyPrice: 1490,
|
||||||
|
currency: "TRY",
|
||||||
|
features: [],
|
||||||
|
limits: { maxAnalyses: 15, maxCoupons: 5 },
|
||||||
|
highlighted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "premium" as PlanType,
|
||||||
|
name: "Premium",
|
||||||
|
description: "",
|
||||||
|
monthlyPrice: 349,
|
||||||
|
yearlyPrice: 3490,
|
||||||
|
currency: "TRY",
|
||||||
|
features: [],
|
||||||
|
limits: { maxAnalyses: 999, maxCoupons: 999 },
|
||||||
|
highlighted: false,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main pricing page content.
|
||||||
|
* Fetches plans from API, shows monthly/yearly toggle,
|
||||||
|
* renders pricing cards and FAQ section.
|
||||||
|
*/
|
||||||
|
export default function PricingContent() {
|
||||||
|
const t = useTranslations("pricing");
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const { data: plansData, isLoading } = useSubscriptionPlans();
|
||||||
|
const { startCheckout, isLoading: checkoutLoading } = usePaddleCheckout();
|
||||||
|
|
||||||
|
const [isYearly, setIsYearly] = useState(false);
|
||||||
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
const [selectedCheckoutPlan, setSelectedCheckoutPlan] =
|
||||||
|
useState<PlanType | null>(null);
|
||||||
|
|
||||||
|
const subtitleColor = useColorModeValue("gray.600", "gray.400");
|
||||||
|
|
||||||
|
const currentPlan = session?.user?.subscriptionPlan ?? "free";
|
||||||
|
const plans = plansData?.data ?? FALLBACK_PLANS;
|
||||||
|
|
||||||
|
// Enrich plans with i18n translated names/descriptions/features
|
||||||
|
const enrichedPlans: readonly PlanInfo[] = plans.map((plan) => {
|
||||||
|
const planKey = String(plan.id).toLowerCase() as
|
||||||
|
| "free"
|
||||||
|
| "plus"
|
||||||
|
| "premium";
|
||||||
|
const featureKeys = getFeatureKeysForPlan(planKey, plan.limits);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...plan,
|
||||||
|
name: t(`plan.${planKey}.name`),
|
||||||
|
description: t(`plan.${planKey}.description`),
|
||||||
|
features: featureKeys.map((key) => {
|
||||||
|
const match = key.match(/^(\d+)\s+(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const count = match[1];
|
||||||
|
const transKey = match[2];
|
||||||
|
return `${count} ${t(`feature.${transKey}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.startsWith("unlimited-")) {
|
||||||
|
const transKey = key.replace("unlimited-", "");
|
||||||
|
return `${t("feature.unlimited")} ${t(`feature.${transKey}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return t(`feature.${key}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePlanSelect = (plan: PlanInfo) => {
|
||||||
|
if (plan.id === "free") return;
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
setLoginModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCheckoutPlan(plan.id);
|
||||||
|
const interval: BillingInterval = isYearly
|
||||||
|
? ("yearly" as BillingInterval)
|
||||||
|
: ("monthly" as BillingInterval);
|
||||||
|
startCheckout(plan.id, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
const faqItems = [
|
||||||
|
{ q: t("faq.q1"), a: t("faq.a1") },
|
||||||
|
{ q: t("faq.q2"), a: t("faq.a2") },
|
||||||
|
{ q: t("faq.q3"), a: t("faq.a3") },
|
||||||
|
{ q: t("faq.q4"), a: t("faq.a4") },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SlideUp>
|
||||||
|
<Box py={{ base: 8, md: 16 }}>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<VStack gap={4} textAlign="center" mb={{ base: 10, md: 14 }}>
|
||||||
|
<Heading
|
||||||
|
as="h1"
|
||||||
|
fontSize={{ base: "3xl", md: "4xl", lg: "5xl" }}
|
||||||
|
fontWeight="900"
|
||||||
|
letterSpacing="-0.03em"
|
||||||
|
lineHeight="1.1"
|
||||||
|
>
|
||||||
|
{t("title")}
|
||||||
|
</Heading>
|
||||||
|
<Text
|
||||||
|
fontSize={{ base: "md", md: "lg" }}
|
||||||
|
color={subtitleColor}
|
||||||
|
maxW="xl"
|
||||||
|
>
|
||||||
|
{t("subtitle")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Monthly / Yearly Toggle */}
|
||||||
|
<HStack
|
||||||
|
gap={3}
|
||||||
|
bg={useColorModeValue("gray.50", "gray.900")}
|
||||||
|
p={1.5}
|
||||||
|
borderRadius="xl"
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
|
<SegmentedControl
|
||||||
|
value={isYearly ? "yearly" : "monthly"}
|
||||||
|
onValueChange={(details) =>
|
||||||
|
setIsYearly(details.value === "yearly")
|
||||||
|
}
|
||||||
|
items={[
|
||||||
|
{ label: t("monthly"), value: "monthly" },
|
||||||
|
{
|
||||||
|
label: `${t("yearly")} 🎁`,
|
||||||
|
value: "yearly",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{isYearly && (
|
||||||
|
<Text fontSize="sm" color="primary.500" fontWeight="semibold">
|
||||||
|
✨ {t("yearly-save")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Pricing Cards */}
|
||||||
|
{isLoading ? (
|
||||||
|
<Flex justify="center" py={20}>
|
||||||
|
<Spinner size="lg" color="primary.500" />
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<SimpleGrid
|
||||||
|
columns={{ base: 1, md: 3 }}
|
||||||
|
gap={{ base: 6, md: 8 }}
|
||||||
|
maxW="5xl"
|
||||||
|
mx="auto"
|
||||||
|
>
|
||||||
|
{enrichedPlans.map((plan) => (
|
||||||
|
<PricingCard
|
||||||
|
key={plan.id}
|
||||||
|
plan={plan}
|
||||||
|
isCurrentPlan={currentPlan === plan.id}
|
||||||
|
isYearly={isYearly}
|
||||||
|
onSelect={handlePlanSelect}
|
||||||
|
isLoading={
|
||||||
|
checkoutLoading && selectedCheckoutPlan === plan.id
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
<Box maxW="3xl" mx="auto" mt={{ base: 16, md: 24 }}>
|
||||||
|
<VStack gap={2} textAlign="center" mb={8}>
|
||||||
|
<HStack gap={2} color="primary.500">
|
||||||
|
<LuMessageCircleQuestion size={24} />
|
||||||
|
<Heading as="h2" size="lg" fontWeight="bold">
|
||||||
|
{t("faq-title")}
|
||||||
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<VStack gap={4} align="stretch">
|
||||||
|
{faqItems.map((item, idx) => (
|
||||||
|
<FaqItem key={idx} question={item.q} answer={item.a} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</SlideUp>
|
||||||
|
|
||||||
|
<LoginModal
|
||||||
|
open={loginModalOpen}
|
||||||
|
onOpenChange={setLoginModalOpen}
|
||||||
|
initialMode="login"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
// Internal helpers
|
||||||
|
// ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function getFeatureKeysForPlan(
|
||||||
|
planKey: "free" | "plus" | "premium",
|
||||||
|
limits: { maxAnalyses: number; maxCoupons: number },
|
||||||
|
): string[] {
|
||||||
|
const analysisLabel =
|
||||||
|
limits.maxAnalyses >= 999
|
||||||
|
? "unlimited-daily-analyses"
|
||||||
|
: `${limits.maxAnalyses} daily-analyses`;
|
||||||
|
const couponLabel =
|
||||||
|
limits.maxCoupons >= 999
|
||||||
|
? "unlimited-daily-coupons"
|
||||||
|
: `${limits.maxCoupons} daily-coupons`;
|
||||||
|
|
||||||
|
const base = [analysisLabel, couponLabel, "basic-analysis"];
|
||||||
|
|
||||||
|
if (planKey === "plus") {
|
||||||
|
return [...base, "detailed-analysis", "h2h-comparison", "coupon-builder"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planKey === "premium") {
|
||||||
|
return [
|
||||||
|
...base,
|
||||||
|
"detailed-analysis",
|
||||||
|
"h2h-comparison",
|
||||||
|
"coupon-builder",
|
||||||
|
"spor-toto",
|
||||||
|
"ad-free",
|
||||||
|
"priority-support",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapsible FAQ item */
|
||||||
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const cardBg = useColorModeValue("white", "gray.900");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
borderRadius="xl"
|
||||||
|
cursor="pointer"
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{ shadow: "md" }}
|
||||||
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<Card.Body p={5}>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Text fontWeight="semibold" fontSize="sm">
|
||||||
|
{question}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
transform={open ? "rotate(180deg)" : "rotate(0deg)"}
|
||||||
|
transition="transform 0.2s"
|
||||||
|
flexShrink={0}
|
||||||
|
ml={3}
|
||||||
|
>
|
||||||
|
<LuChevronDown size={18} />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
{open && (
|
||||||
|
<Text mt={3} fontSize="sm" color="fg.muted" lineHeight="1.7">
|
||||||
|
{answer}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import * as yup from "yup";
|
|||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { PasswordInput } from "@/components/ui/forms/password-input";
|
import { PasswordInput } from "@/components/ui/forms/password-input";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { SubscriptionCard } from "@/components/subscription";
|
||||||
|
|
||||||
interface InfoRowProps {
|
interface InfoRowProps {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -174,6 +175,9 @@ export default function ProfileContent() {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
{/* Subscription Info */}
|
||||||
|
<SubscriptionCard />
|
||||||
|
|
||||||
{/* Account Info */}
|
{/* Account Info */}
|
||||||
<Card.Root
|
<Card.Root
|
||||||
bg={cardBg}
|
bg={cardBg}
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ function HomeCard() {
|
|||||||
const imagesCollection = createListCollection({ items: images });
|
const imagesCollection = createListCollection({ items: images });
|
||||||
|
|
||||||
const currentImage = imagesCollection.items.find(
|
const currentImage = imagesCollection.items.find(
|
||||||
(img) => img.value === selectedImage
|
(img) => img.value === selectedImage,
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeCollection = createTreeCollection<Node>({
|
const nodeCollection = createTreeCollection<Node>({
|
||||||
@@ -410,7 +410,7 @@ function HomeCard() {
|
|||||||
|
|
||||||
const [tabs, setTabs] = React.useState<Item[]>(itemsTabs);
|
const [tabs, setTabs] = React.useState<Item[]>(itemsTabs);
|
||||||
const [selectedTab, setSelectedTab] = React.useState<string | null>(
|
const [selectedTab, setSelectedTab] = React.useState<string | null>(
|
||||||
itemsTabs[0].id
|
itemsTabs[0].id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const uuid = () => {
|
const uuid = () => {
|
||||||
@@ -2682,7 +2682,7 @@ function HomeCard() {
|
|||||||
}
|
}
|
||||||
onCheckedChange={(changes) => {
|
onCheckedChange={(changes) => {
|
||||||
setSelection(
|
setSelection(
|
||||||
changes.checked ? items.map((item) => item.name) : []
|
changes.checked ? items.map((item) => item.name) : [],
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -2710,7 +2710,7 @@ function HomeCard() {
|
|||||||
setSelection((prev) =>
|
setSelection((prev) =>
|
||||||
changes.checked
|
changes.checked
|
||||||
? [...prev, item.name]
|
? [...prev, item.name]
|
||||||
: selection.filter((name) => name !== item.name)
|
: selection.filter((name) => name !== item.name),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import {
|
|||||||
useSyncBulletin,
|
useSyncBulletin,
|
||||||
useRolloverHistory,
|
useRolloverHistory,
|
||||||
} from "@/lib/api/spor-toto/use-hooks";
|
} from "@/lib/api/spor-toto/use-hooks";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { UsersQueryKeys } from "@/lib/api/users/use-hooks";
|
||||||
import type {
|
import type {
|
||||||
SporTotoBulletinDto,
|
SporTotoBulletinDto,
|
||||||
SporTotoPredictionResultDto,
|
SporTotoPredictionResultDto,
|
||||||
@@ -59,15 +61,11 @@ export default function SporTotoContent() {
|
|||||||
const rolloverHistory = useRolloverHistory(10);
|
const rolloverHistory = useRolloverHistory(10);
|
||||||
const syncBulletin = useSyncBulletin();
|
const syncBulletin = useSyncBulletin();
|
||||||
const generatePrediction = useGeneratePrediction();
|
const generatePrediction = useGeneratePrediction();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const toast = (opts: { title: string; status: string }) =>
|
const toast = (opts: { title: string; status: string }) =>
|
||||||
toaster.create({
|
toaster.create({
|
||||||
title: opts.title,
|
title: opts.title,
|
||||||
type: opts.status as
|
type: opts.status as "success" | "warning" | "error" | "info" | "loading",
|
||||||
| "success"
|
|
||||||
| "warning"
|
|
||||||
| "error"
|
|
||||||
| "info"
|
|
||||||
| "loading",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSync = async () => {
|
const handleSync = async () => {
|
||||||
@@ -91,6 +89,7 @@ export default function SporTotoContent() {
|
|||||||
bulletinId: selectedBulletin,
|
bulletinId: selectedBulletin,
|
||||||
strategy,
|
strategy,
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: UsersQueryKeys.me() });
|
||||||
toast({
|
toast({
|
||||||
title: t("prediction-generated"),
|
title: t("prediction-generated"),
|
||||||
status: "success",
|
status: "success",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { PlanBadge } from "./plan-badge";
|
||||||
|
export { PricingCard } from "./pricing-card";
|
||||||
|
export { SubscriptionCard } from "./subscription-card";
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Badge } from "@chakra-ui/react";
|
||||||
|
import type { PlanType } from "@/lib/api/subscriptions/types";
|
||||||
|
|
||||||
|
interface PlanBadgeProps {
|
||||||
|
plan: PlanType | "free" | "plus" | "premium";
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
const planColorMap: Record<string, string> = {
|
||||||
|
free: "gray",
|
||||||
|
plus: "blue",
|
||||||
|
premium: "yellow",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A colored badge showing the user's subscription plan.
|
||||||
|
* Uses Chakra v3 Badge component with colorPalette.
|
||||||
|
*/
|
||||||
|
export function PlanBadge({ plan, size = "sm" }: PlanBadgeProps) {
|
||||||
|
const planKey = String(plan).toLowerCase();
|
||||||
|
const colorPalette = planColorMap[planKey] ?? "gray";
|
||||||
|
const label = planKey.charAt(0).toUpperCase() + planKey.slice(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
colorPalette={colorPalette}
|
||||||
|
variant="solid"
|
||||||
|
size={size}
|
||||||
|
borderRadius="full"
|
||||||
|
px={2}
|
||||||
|
fontWeight="bold"
|
||||||
|
letterSpacing="0.02em"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import type { PlanInfo } from "@/lib/api/subscriptions/types";
|
||||||
|
import { LuCheck, LuZap, LuCrown, LuRocket } from "react-icons/lu";
|
||||||
|
|
||||||
|
interface PricingCardProps {
|
||||||
|
plan: PlanInfo;
|
||||||
|
isCurrentPlan: boolean;
|
||||||
|
isYearly: boolean;
|
||||||
|
onSelect: (plan: PlanInfo) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const planIconMap: Record<string, React.ReactNode> = {
|
||||||
|
free: <LuZap size={20} />,
|
||||||
|
plus: <LuRocket size={20} />,
|
||||||
|
premium: <LuCrown size={20} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual pricing card for a subscription plan.
|
||||||
|
* Shows plan details, features, price, and CTA button.
|
||||||
|
*/
|
||||||
|
export function PricingCard({
|
||||||
|
plan,
|
||||||
|
isCurrentPlan,
|
||||||
|
isYearly,
|
||||||
|
onSelect,
|
||||||
|
isLoading,
|
||||||
|
}: PricingCardProps) {
|
||||||
|
const t = useTranslations("pricing");
|
||||||
|
|
||||||
|
const highlightBorder = useColorModeValue("primary.500", "primary.400");
|
||||||
|
const cardBg = useColorModeValue("white", "gray.900");
|
||||||
|
const mutedText = useColorModeValue("gray.500", "gray.400");
|
||||||
|
|
||||||
|
const price = isYearly ? plan.yearlyPrice : plan.monthlyPrice;
|
||||||
|
const displayPrice = isYearly
|
||||||
|
? Math.round(plan.yearlyPrice / 12)
|
||||||
|
: plan.monthlyPrice;
|
||||||
|
const isFree = plan.id === "free";
|
||||||
|
|
||||||
|
const buttonLabel = isCurrentPlan
|
||||||
|
? t("current-plan")
|
||||||
|
: isFree
|
||||||
|
? t("get-started")
|
||||||
|
: t("upgrade");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
borderWidth={plan.highlighted ? "2px" : "1px"}
|
||||||
|
borderColor={plan.highlighted ? highlightBorder : "border.muted"}
|
||||||
|
borderRadius="2xl"
|
||||||
|
position="relative"
|
||||||
|
overflow="visible"
|
||||||
|
transition="all 0.3s ease"
|
||||||
|
_hover={{
|
||||||
|
transform: "translateY(-4px)",
|
||||||
|
shadow: plan.highlighted ? "2xl" : "lg",
|
||||||
|
}}
|
||||||
|
height="full"
|
||||||
|
>
|
||||||
|
{/* Most Popular Badge */}
|
||||||
|
{plan.highlighted && (
|
||||||
|
<Badge
|
||||||
|
position="absolute"
|
||||||
|
top="-3"
|
||||||
|
left="50%"
|
||||||
|
transform="translateX(-50%)"
|
||||||
|
colorPalette="primary"
|
||||||
|
variant="solid"
|
||||||
|
borderRadius="full"
|
||||||
|
px={4}
|
||||||
|
py={1}
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
>
|
||||||
|
{t("most-popular")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card.Body p={6}>
|
||||||
|
<VStack gap={6} align="stretch" height="full">
|
||||||
|
{/* Plan Header */}
|
||||||
|
<VStack gap={2} align="start">
|
||||||
|
<HStack gap={2}>
|
||||||
|
<Box
|
||||||
|
p={2}
|
||||||
|
borderRadius="lg"
|
||||||
|
bg={plan.highlighted ? "primary.50" : "gray.50"}
|
||||||
|
_dark={{
|
||||||
|
bg: plan.highlighted ? "primary.950" : "gray.800",
|
||||||
|
}}
|
||||||
|
color={plan.highlighted ? "primary.500" : "fg.muted"}
|
||||||
|
>
|
||||||
|
{planIconMap[plan.id] ?? <LuZap size={20} />}
|
||||||
|
</Box>
|
||||||
|
<Heading as="h3" size="lg" fontWeight="bold">
|
||||||
|
{plan.name}
|
||||||
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" color={mutedText} minH="10">
|
||||||
|
{plan.description}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<Box>
|
||||||
|
{isFree ? (
|
||||||
|
<VStack gap={0} align="start">
|
||||||
|
<HStack gap={1} align="baseline">
|
||||||
|
<Text fontSize="4xl" fontWeight="900" lineHeight="1">
|
||||||
|
{plan.currency === "TRY" ? "₺" : "$"}0
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="xs" color={mutedText}>
|
||||||
|
{t("free-forever")}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<VStack gap={0} align="start">
|
||||||
|
<HStack gap={1} align="baseline">
|
||||||
|
<Text fontSize="4xl" fontWeight="900" lineHeight="1">
|
||||||
|
{plan.currency === "TRY" ? "₺" : "$"}
|
||||||
|
{displayPrice}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={mutedText}>
|
||||||
|
{t("per-month")}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{isYearly && (
|
||||||
|
<Text fontSize="xs" color={mutedText}>
|
||||||
|
{plan.currency === "TRY" ? "₺" : "$"}
|
||||||
|
{price}
|
||||||
|
{t("per-year")} · {t("billed-yearly")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Features List */}
|
||||||
|
<VStack gap={3} align="start" flex={1}>
|
||||||
|
{plan.features.map((feature) => (
|
||||||
|
<HStack key={feature} gap={2} align="start">
|
||||||
|
<Box
|
||||||
|
color={plan.highlighted ? "primary.500" : "green.500"}
|
||||||
|
mt="0.5"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<LuCheck size={16} />
|
||||||
|
</Box>
|
||||||
|
<Text fontSize="sm">{feature}</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Button
|
||||||
|
w="full"
|
||||||
|
size="lg"
|
||||||
|
borderRadius="xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
variant={
|
||||||
|
isCurrentPlan ? "outline" : plan.highlighted ? "solid" : "outline"
|
||||||
|
}
|
||||||
|
colorPalette={plan.highlighted ? "primary" : "gray"}
|
||||||
|
disabled={isCurrentPlan}
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={() => onSelect(plan)}
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Spinner,
|
||||||
|
HStack,
|
||||||
|
Textarea,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import {
|
||||||
|
useMySubscription,
|
||||||
|
useCancelSubscription,
|
||||||
|
} from "@/lib/api/subscriptions/use-hooks";
|
||||||
|
import { PlanBadge } from "./plan-badge";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LuCalendar, LuTriangleAlert, LuX, LuCheck } from "react-icons/lu";
|
||||||
|
import { useRouter } from "@/i18n/navigation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription info card for the Profile page.
|
||||||
|
* Shows current plan, billing dates, cancel option.
|
||||||
|
*/
|
||||||
|
export function SubscriptionCard() {
|
||||||
|
const t = useTranslations("subscription");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const { data: subData, isLoading } = useMySubscription(!!session);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
|
|
||||||
|
const [showCancelForm, setShowCancelForm] = useState(false);
|
||||||
|
const [cancelReason, setCancelReason] = useState("");
|
||||||
|
const cancelMutation = useCancelSubscription();
|
||||||
|
|
||||||
|
const subscription = subData?.data ?? null;
|
||||||
|
const plan = subscription?.plan ?? session?.user?.subscriptionPlan ?? "free";
|
||||||
|
const isFree = plan === "free";
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
await cancelMutation.mutateAsync({ reason: cancelReason || undefined });
|
||||||
|
setShowCancelForm(false);
|
||||||
|
setCancelReason("");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Body>
|
||||||
|
<Flex justify="center" py={8}>
|
||||||
|
<Spinner size="sm" color="primary.500" />
|
||||||
|
</Flex>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||||
|
<Card.Header>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
{t("title")}
|
||||||
|
</Heading>
|
||||||
|
<PlanBadge plan={plan} />
|
||||||
|
</Flex>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body pt={0}>
|
||||||
|
<VStack gap={4} align="stretch">
|
||||||
|
{/* Subscription Details */}
|
||||||
|
{subscription && !isFree && (
|
||||||
|
<>
|
||||||
|
{subscription.currentPeriodEnd && (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<HStack gap={2} color="fg.muted">
|
||||||
|
<LuCalendar size={14} />
|
||||||
|
<Text fontSize="sm">{t("next-billing")}</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
|
{new Date(
|
||||||
|
subscription.currentPeriodEnd,
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subscription.cancelEffectiveDate && (
|
||||||
|
<Box
|
||||||
|
p={3}
|
||||||
|
bg="orange.50"
|
||||||
|
_dark={{ bg: "orange.950" }}
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<HStack gap={2}>
|
||||||
|
<LuTriangleAlert size={14} color="orange" />
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
color="orange.600"
|
||||||
|
_dark={{ color: "orange.300" }}
|
||||||
|
>
|
||||||
|
{t("cancelled-info", {
|
||||||
|
date: new Date(
|
||||||
|
subscription.cancelEffectiveDate,
|
||||||
|
).toLocaleDateString(),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Flex gap={2} direction={{ base: "column", sm: "row" }}>
|
||||||
|
{isFree ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push("/pricing")}
|
||||||
|
colorPalette="primary"
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
borderRadius="lg"
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
{t("upgrade-cta")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => router.push("/pricing")}
|
||||||
|
colorPalette="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
borderRadius="lg"
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
{t("manage")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!subscription?.cancelEffectiveDate && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
colorPalette="red"
|
||||||
|
onClick={() => setShowCancelForm(true)}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Cancel Confirmation */}
|
||||||
|
{showCancelForm && (
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="red.200"
|
||||||
|
_dark={{ borderColor: "red.800" }}
|
||||||
|
borderRadius="lg"
|
||||||
|
>
|
||||||
|
<VStack gap={3} align="stretch">
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="semibold"
|
||||||
|
color="red.600"
|
||||||
|
_dark={{ color: "red.300" }}
|
||||||
|
>
|
||||||
|
{t("cancel-confirm-title")}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color="fg.muted">
|
||||||
|
{t("cancel-confirm-message")}
|
||||||
|
</Text>
|
||||||
|
<Textarea
|
||||||
|
size="sm"
|
||||||
|
placeholder={t("cancel-reason-placeholder")}
|
||||||
|
value={cancelReason}
|
||||||
|
onChange={(e) => setCancelReason(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<HStack gap={2} justify="flex-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCancelForm(false);
|
||||||
|
setCancelReason("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LuX />
|
||||||
|
{tCommon("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorPalette="red"
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
loading={cancelMutation.isPending}
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
<LuCheck />
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Card.Body>
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,7 +19,12 @@ import { useSession } from "next-auth/react";
|
|||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import { SlideUp, FadeIn } from "@/components/motion";
|
import { SlideUp, FadeIn } from "@/components/motion";
|
||||||
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
|
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
|
||||||
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
|
import {
|
||||||
|
LuArrowLeft,
|
||||||
|
LuCalendar,
|
||||||
|
LuTrophy,
|
||||||
|
LuChevronDown,
|
||||||
|
} from "react-icons/lu";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { LoginModal } from "@/components/auth/login-modal";
|
import { LoginModal } from "@/components/auth/login-modal";
|
||||||
@@ -29,7 +34,8 @@ import { LoginModal } from "@/components/auth/login-modal";
|
|||||||
// ─────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────
|
||||||
|
|
||||||
function getMatchTimestamp(match: MatchResponseDto): number {
|
function getMatchTimestamp(match: MatchResponseDto): number {
|
||||||
const raw = typeof match.mstUtc === "string" ? Number(match.mstUtc) : match.mstUtc;
|
const raw =
|
||||||
|
typeof match.mstUtc === "string" ? Number(match.mstUtc) : match.mstUtc;
|
||||||
return Number.isFinite(raw) ? raw : 0;
|
return Number.isFinite(raw) ? raw : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,19 +45,32 @@ function getMatchStatus(match: MatchResponseDto): string {
|
|||||||
|
|
||||||
function isMatchFinished(match: MatchResponseDto): boolean {
|
function isMatchFinished(match: MatchResponseDto): boolean {
|
||||||
const status = getMatchStatus(match);
|
const status = getMatchStatus(match);
|
||||||
return status === "FT" || status === "FINISHED" || status === "POSTGAME" || status === "POST_GAME";
|
return (
|
||||||
|
status === "FT" ||
|
||||||
|
status === "FINISHED" ||
|
||||||
|
status === "POSTGAME" ||
|
||||||
|
status === "POST_GAME"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMatchLive(match: MatchResponseDto): boolean {
|
function isMatchLive(match: MatchResponseDto): boolean {
|
||||||
const status = getMatchStatus(match);
|
const status = getMatchStatus(match);
|
||||||
return status === "LIVE" || status === "INPROGRESS" || status === "IN_PROGRESS";
|
return (
|
||||||
|
status === "LIVE" || status === "INPROGRESS" || status === "IN_PROGRESS"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTeamSideName(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
|
function getTeamSideName(
|
||||||
|
team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"],
|
||||||
|
fallback?: unknown,
|
||||||
|
): string {
|
||||||
return String(team?.name || fallback || "");
|
return String(team?.name || fallback || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTeamSideLogo(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
|
function getTeamSideLogo(
|
||||||
|
team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"],
|
||||||
|
fallback?: unknown,
|
||||||
|
): string {
|
||||||
return String(team?.logo || fallback || "");
|
return String(team?.logo || fallback || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +93,10 @@ const SEASONS = (() => {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const currentMonth = new Date().getMonth() + 1;
|
const currentMonth = new Date().getMonth() + 1;
|
||||||
const startYear = currentMonth >= 8 ? currentYear : currentYear - 1;
|
const startYear = currentMonth >= 8 ? currentYear : currentYear - 1;
|
||||||
return Array.from({ length: 5 }, (_, i) => `${startYear - i}-${startYear - i + 1}`);
|
return Array.from(
|
||||||
|
{ length: 5 },
|
||||||
|
(_, i) => `${startYear - i}-${startYear - i + 1}`,
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────
|
||||||
@@ -97,7 +119,11 @@ export default function TeamDetailContent() {
|
|||||||
data: matchesResponse,
|
data: matchesResponse,
|
||||||
isLoading: matchesLoading,
|
isLoading: matchesLoading,
|
||||||
isFetching: matchesFetching,
|
isFetching: matchesFetching,
|
||||||
} = useTeamMatches(teamId, { page: currentPage, limit: 20, season: activeSeason });
|
} = useTeamMatches(teamId, {
|
||||||
|
page: currentPage,
|
||||||
|
limit: 20,
|
||||||
|
season: activeSeason,
|
||||||
|
});
|
||||||
|
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
@@ -109,20 +135,30 @@ export default function TeamDetailContent() {
|
|||||||
const team = teamWrapper?.data as Record<string, unknown> | undefined;
|
const team = teamWrapper?.data as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
// matchesResponse = { success, status, message, data: { data: [...], total, page, limit, totalPages } }
|
// matchesResponse = { success, status, message, data: { data: [...], total, page, limit, totalPages } }
|
||||||
const paginationWrapper = matchesResponse as Record<string, unknown> | undefined;
|
const paginationWrapper = matchesResponse as
|
||||||
const paginationData = paginationWrapper?.data as Record<string, unknown> | undefined;
|
| Record<string, unknown>
|
||||||
const matches: MatchResponseDto[] = (Array.isArray(paginationData?.data) ? paginationData.data : paginationData?.data ? [] : []) as MatchResponseDto[];
|
| undefined;
|
||||||
|
const paginationData = paginationWrapper?.data as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
const matches: MatchResponseDto[] = (
|
||||||
|
Array.isArray(paginationData?.data)
|
||||||
|
? paginationData.data
|
||||||
|
: paginationData?.data
|
||||||
|
? []
|
||||||
|
: []
|
||||||
|
) as MatchResponseDto[];
|
||||||
const totalPages = (paginationData?.totalPages as number) ?? 1;
|
const totalPages = (paginationData?.totalPages as number) ?? 1;
|
||||||
const totalMatches = (paginationData?.total as number) ?? 0;
|
const totalMatches = (paginationData?.total as number) ?? 0;
|
||||||
|
|
||||||
// Separate past and upcoming matches
|
// Separate past and upcoming matches
|
||||||
const pastMatches = useMemo(
|
const pastMatches = useMemo(
|
||||||
() => matches.filter((m) => isMatchFinished(m)),
|
() => matches.filter((m) => isMatchFinished(m)),
|
||||||
[matches]
|
[matches],
|
||||||
);
|
);
|
||||||
const upcomingMatches = useMemo(
|
const upcomingMatches = useMemo(
|
||||||
() => matches.filter((m) => !isMatchFinished(m)),
|
() => matches.filter((m) => !isMatchFinished(m)),
|
||||||
[matches]
|
[matches],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pagination handlers
|
// Pagination handlers
|
||||||
@@ -169,7 +205,9 @@ export default function TeamDetailContent() {
|
|||||||
if (!team) {
|
if (!team) {
|
||||||
return (
|
return (
|
||||||
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
|
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
|
||||||
<Text color="fg.muted" fontSize="lg">Takım bulunamadı</Text>
|
<Text color="fg.muted" fontSize="lg">
|
||||||
|
Takım bulunamadı
|
||||||
|
</Text>
|
||||||
<Button variant="outline" onClick={() => router.back()}>
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
<LuArrowLeft /> Geri
|
<LuArrowLeft /> Geri
|
||||||
</Button>
|
</Button>
|
||||||
@@ -181,13 +219,24 @@ export default function TeamDetailContent() {
|
|||||||
<SlideUp>
|
<SlideUp>
|
||||||
<Box>
|
<Box>
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<Button variant="ghost" size="sm" mb={4} onClick={() => router.back()} gap={1.5}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
mb={4}
|
||||||
|
onClick={() => router.back()}
|
||||||
|
gap={1.5}
|
||||||
|
>
|
||||||
<LuArrowLeft />
|
<LuArrowLeft />
|
||||||
Geri
|
Geri
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Team Header */}
|
{/* Team Header */}
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
mb={6}
|
||||||
|
>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<HStack gap={6} justify="center" align="center">
|
<HStack gap={6} justify="center" align="center">
|
||||||
{(team as Record<string, unknown>).logo ? (
|
{(team as Record<string, unknown>).logo ? (
|
||||||
@@ -206,7 +255,9 @@ export default function TeamDetailContent() {
|
|||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Text fontSize="3xl" fontWeight="bold" color="primary.fg">
|
<Text fontSize="3xl" fontWeight="bold" color="primary.fg">
|
||||||
{String((team as Record<string, unknown>).name || "T").charAt(0)}
|
{String(
|
||||||
|
(team as Record<string, unknown>).name || "T",
|
||||||
|
).charAt(0)}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -249,7 +300,11 @@ export default function TeamDetailContent() {
|
|||||||
cardBg={cardBg}
|
cardBg={cardBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
statusBadge={getStatusBadge(match)}
|
statusBadge={getStatusBadge(match)}
|
||||||
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
onClick={() =>
|
||||||
|
session
|
||||||
|
? router.push(`/matches/${match.id}`)
|
||||||
|
: setLoginModalOpen(true)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -260,7 +315,13 @@ export default function TeamDetailContent() {
|
|||||||
{/* Past Matches — Season Grouped */}
|
{/* Past Matches — Season Grouped */}
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex align="center" justify="space-between" mb={4} flexWrap="wrap" gap={2}>
|
<Flex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
mb={4}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
<Heading as="h2" size="lg">
|
<Heading as="h2" size="lg">
|
||||||
📊 Geçmiş Maçlar
|
📊 Geçmiş Maçlar
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -320,7 +381,11 @@ export default function TeamDetailContent() {
|
|||||||
cardBg={cardBg}
|
cardBg={cardBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
statusBadge={getStatusBadge(match)}
|
statusBadge={getStatusBadge(match)}
|
||||||
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
onClick={() =>
|
||||||
|
session
|
||||||
|
? router.push(`/matches/${match.id}`)
|
||||||
|
: setLoginModalOpen(true)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -357,7 +422,9 @@ export default function TeamDetailContent() {
|
|||||||
key={pageNum}
|
key={pageNum}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={pageNum === currentPage ? "solid" : "ghost"}
|
variant={pageNum === currentPage ? "solid" : "ghost"}
|
||||||
bg={pageNum === currentPage ? seasonActiveBg : undefined}
|
bg={
|
||||||
|
pageNum === currentPage ? seasonActiveBg : undefined
|
||||||
|
}
|
||||||
color={pageNum === currentPage ? "white" : undefined}
|
color={pageNum === currentPage ? "white" : undefined}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
minW="36px"
|
minW="36px"
|
||||||
@@ -407,7 +474,13 @@ interface MatchRowProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MatchRow({ match, cardBg, borderColor, statusBadge, onClick }: MatchRowProps) {
|
function MatchRow({
|
||||||
|
match,
|
||||||
|
cardBg,
|
||||||
|
borderColor,
|
||||||
|
statusBadge,
|
||||||
|
onClick,
|
||||||
|
}: MatchRowProps) {
|
||||||
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
||||||
const matchTimestamp = getMatchTimestamp(match);
|
const matchTimestamp = getMatchTimestamp(match);
|
||||||
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
|
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
|
||||||
@@ -436,17 +509,34 @@ function MatchRow({ match, cardBg, borderColor, statusBadge, onClick }: MatchRow
|
|||||||
{homeTeamName}
|
{homeTeamName}
|
||||||
</Text>
|
</Text>
|
||||||
{homeTeamLogo ? (
|
{homeTeamLogo ? (
|
||||||
<Image src={homeTeamLogo} alt="" boxSize="24px" objectFit="contain" flexShrink={0} />
|
<Image
|
||||||
|
src={homeTeamLogo}
|
||||||
|
alt=""
|
||||||
|
boxSize="24px"
|
||||||
|
objectFit="contain"
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Flex boxSize="24px" bg="primary.subtle" borderRadius="full" align="center" justify="center" flexShrink={0}>
|
<Flex
|
||||||
<Text fontSize="xs" fontWeight="bold">{homeTeamName?.charAt(0)}</Text>
|
boxSize="24px"
|
||||||
|
bg="primary.subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" fontWeight="bold">
|
||||||
|
{homeTeamName?.charAt(0)}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* Score / VS */}
|
{/* Score / VS */}
|
||||||
<VStack gap={0} flexShrink={0} minW="60px">
|
<VStack gap={0} flexShrink={0} minW="60px">
|
||||||
{hasScore && match.scoreHome !== undefined && match.scoreHome !== null ? (
|
{hasScore &&
|
||||||
|
match.scoreHome !== undefined &&
|
||||||
|
match.scoreHome !== null ? (
|
||||||
<Text fontSize="md" fontWeight="900">
|
<Text fontSize="md" fontWeight="900">
|
||||||
{match.scoreHome} - {match.scoreAway}
|
{match.scoreHome} - {match.scoreAway}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -468,10 +558,25 @@ function MatchRow({ match, cardBg, borderColor, statusBadge, onClick }: MatchRow
|
|||||||
{/* Away Team */}
|
{/* Away Team */}
|
||||||
<HStack gap={2} flex={1}>
|
<HStack gap={2} flex={1}>
|
||||||
{awayTeamLogo ? (
|
{awayTeamLogo ? (
|
||||||
<Image src={awayTeamLogo} alt="" boxSize="24px" objectFit="contain" flexShrink={0} />
|
<Image
|
||||||
|
src={awayTeamLogo}
|
||||||
|
alt=""
|
||||||
|
boxSize="24px"
|
||||||
|
objectFit="contain"
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Flex boxSize="24px" bg="primary.subtle" borderRadius="full" align="center" justify="center" flexShrink={0}>
|
<Flex
|
||||||
<Text fontSize="xs" fontWeight="bold">{awayTeamName?.charAt(0)}</Text>
|
boxSize="24px"
|
||||||
|
bg="primary.subtle"
|
||||||
|
borderRadius="full"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<Text fontSize="xs" fontWeight="bold">
|
||||||
|
{awayTeamName?.charAt(0)}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<Text fontSize="sm" fontWeight="600" truncate>
|
<Text fontSize="sm" fontWeight="600" truncate>
|
||||||
@@ -483,7 +588,11 @@ function MatchRow({ match, cardBg, borderColor, statusBadge, onClick }: MatchRow
|
|||||||
{/* Status + League */}
|
{/* Status + League */}
|
||||||
<HStack gap={2} flexShrink={0} ml={3}>
|
<HStack gap={2} flexShrink={0} ml={3}>
|
||||||
{leagueLabel && (
|
{leagueLabel && (
|
||||||
<Text fontSize="2xs" color="fg.muted" display={{ base: "none", md: "block" }}>
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
color="fg.muted"
|
||||||
|
display={{ base: "none", md: "block" }}
|
||||||
|
>
|
||||||
{leagueLabel}
|
{leagueLabel}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ export default function TeamsContent() {
|
|||||||
<Spinner size="lg" color="primary.500" />
|
<Spinner size="lg" color="primary.500" />
|
||||||
</Flex>
|
</Flex>
|
||||||
) : query.length < 2 ? (
|
) : query.length < 2 ? (
|
||||||
<Flex justify="center" py={16} direction="column" align="center" gap={3}>
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
py={16}
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
gap={3}
|
||||||
|
>
|
||||||
<Text fontSize="5xl">⚽</Text>
|
<Text fontSize="5xl">⚽</Text>
|
||||||
<Text color="fg.muted" fontSize="lg">
|
<Text color="fg.muted" fontSize="lg">
|
||||||
Aramak istediğiniz takımın adını yazın
|
Aramak istediğiniz takımın adını yazın
|
||||||
@@ -117,7 +123,11 @@ export default function TeamsContent() {
|
|||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Text fontSize="xl" fontWeight="bold" color="primary.fg">
|
<Text
|
||||||
|
fontSize="xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="primary.fg"
|
||||||
|
>
|
||||||
{team.name?.charAt(0) || "T"}
|
{team.name?.charAt(0) || "T"}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { Icon, IconButton, Presence } from '@chakra-ui/react';
|
import { Icon, IconButton, Presence } from "@chakra-ui/react";
|
||||||
import { FiChevronUp } from 'react-icons/fi';
|
import { FiChevronUp } from "react-icons/fi";
|
||||||
|
|
||||||
const BackToTop = () => {
|
const BackToTop = () => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
@@ -12,14 +12,14 @@ const BackToTop = () => {
|
|||||||
setIsVisible(window.pageYOffset > 300);
|
setIsVisible(window.pageYOffset > 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener("scroll", handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,19 +27,19 @@ const BackToTop = () => {
|
|||||||
<Presence
|
<Presence
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
present={isVisible}
|
present={isVisible}
|
||||||
animationName={{ _open: 'fade-in', _closed: 'fade-out' }}
|
animationName={{ _open: "fade-in", _closed: "fade-out" }}
|
||||||
animationDuration='moderate'
|
animationDuration="moderate"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={{ base: 'solid', _dark: 'subtle' }}
|
variant={{ base: "solid", _dark: "subtle" }}
|
||||||
aria-label='Back to top'
|
aria-label="Back to top"
|
||||||
position='fixed'
|
position="fixed"
|
||||||
bottom='8'
|
bottom="8"
|
||||||
right='8'
|
right="8"
|
||||||
borderRadius='full'
|
borderRadius="full"
|
||||||
size='lg'
|
size="lg"
|
||||||
shadow='lg'
|
shadow="lg"
|
||||||
zIndex='999'
|
zIndex="999"
|
||||||
onClick={scrollToTop}
|
onClick={scrollToTop}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
|
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react";
|
||||||
import { AbsoluteCenter, Button as ChakraButton, Span, Spinner } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
AbsoluteCenter,
|
||||||
|
Button as ChakraButton,
|
||||||
|
Span,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
interface ButtonLoadingProps {
|
interface ButtonLoadingProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -9,20 +14,21 @@ interface ButtonLoadingProps {
|
|||||||
|
|
||||||
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
||||||
|
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(props, ref) {
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function Button(props, ref) {
|
||||||
const { loading, disabled, loadingText, children, ...rest } = props;
|
const { loading, disabled, loadingText, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
||||||
{loading && !loadingText ? (
|
{loading && !loadingText ? (
|
||||||
<>
|
<>
|
||||||
<AbsoluteCenter display='inline-flex'>
|
<AbsoluteCenter display="inline-flex">
|
||||||
<Spinner size='inherit' color='inherit' />
|
<Spinner size="inherit" color="inherit" />
|
||||||
</AbsoluteCenter>
|
</AbsoluteCenter>
|
||||||
<Span opacity={0}>{children}</Span>
|
<Span opacity={0}>{children}</Span>
|
||||||
</>
|
</>
|
||||||
) : loading && loadingText ? (
|
) : loading && loadingText ? (
|
||||||
<>
|
<>
|
||||||
<Spinner size='inherit' color='inherit' />
|
<Spinner size="inherit" color="inherit" />
|
||||||
{loadingText}
|
{loadingText}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -30,4 +36,5 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function
|
|||||||
)}
|
)}
|
||||||
</ChakraButton>
|
</ChakraButton>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import type { ButtonProps } from '@chakra-ui/react';
|
import type { ButtonProps } from "@chakra-ui/react";
|
||||||
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
|
import { IconButton as ChakraIconButton } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LuX } from 'react-icons/lu';
|
import { LuX } from "react-icons/lu";
|
||||||
|
|
||||||
export type CloseButtonProps = ButtonProps;
|
export type CloseButtonProps = ButtonProps;
|
||||||
|
|
||||||
export const CloseButton = React.forwardRef<HTMLButtonElement, CloseButtonProps>(function CloseButton(props, ref) {
|
export const CloseButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
CloseButtonProps
|
||||||
|
>(function CloseButton(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraIconButton variant='ghost' aria-label='Close' ref={ref} {...props}>
|
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||||
{props.children ?? <LuX />}
|
{props.children ?? <LuX />}
|
||||||
</ChakraIconButton>
|
</ChakraIconButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { HTMLChakraProps, RecipeProps } from '@chakra-ui/react';
|
import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react";
|
||||||
import { createRecipeContext } from '@chakra-ui/react';
|
import { createRecipeContext } from "@chakra-ui/react";
|
||||||
|
|
||||||
export interface LinkButtonProps extends HTMLChakraProps<'a', RecipeProps<'button'>> {}
|
export interface LinkButtonProps extends HTMLChakraProps<
|
||||||
|
"a",
|
||||||
|
RecipeProps<"button">
|
||||||
|
> {}
|
||||||
|
|
||||||
const { withContext } = createRecipeContext({ key: 'button' });
|
const { withContext } = createRecipeContext({ key: "button" });
|
||||||
|
|
||||||
// Replace "a" with your framework's link component
|
// Replace "a" with your framework's link component
|
||||||
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>('a');
|
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>("a");
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { ButtonProps } from '@chakra-ui/react';
|
import type { ButtonProps } from "@chakra-ui/react";
|
||||||
import { Button, Toggle as ChakraToggle, useToggleContext } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
Button,
|
||||||
|
Toggle as ChakraToggle,
|
||||||
|
useToggleContext,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
interface ToggleProps extends ChakraToggle.RootProps {
|
interface ToggleProps extends ChakraToggle.RootProps {
|
||||||
variant?: keyof typeof variantMap;
|
variant?: keyof typeof variantMap;
|
||||||
size?: ButtonProps['size'];
|
size?: ButtonProps["size"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantMap = {
|
const variantMap = {
|
||||||
solid: { on: 'solid', off: 'outline' },
|
solid: { on: "solid", off: "outline" },
|
||||||
surface: { on: 'surface', off: 'outline' },
|
surface: { on: "surface", off: "outline" },
|
||||||
subtle: { on: 'subtle', off: 'ghost' },
|
subtle: { on: "subtle", off: "ghost" },
|
||||||
ghost: { on: 'subtle', off: 'ghost' },
|
ghost: { on: "subtle", off: "ghost" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(function Toggle(props, ref) {
|
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
||||||
const { variant = 'subtle', size, children, ...rest } = props;
|
function Toggle(props, ref) {
|
||||||
|
const { variant = "subtle", size, children, ...rest } = props;
|
||||||
const variantConfig = variantMap[variant];
|
const variantConfig = variantMap[variant];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,18 +32,26 @@ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(function
|
|||||||
</ToggleBaseButton>
|
</ToggleBaseButton>
|
||||||
</ChakraToggle.Root>
|
</ChakraToggle.Root>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
interface ToggleBaseButtonProps extends Omit<ButtonProps, 'variant'> {
|
|
||||||
variant: Record<'on' | 'off', ButtonProps['variant']>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ToggleBaseButton = React.forwardRef<HTMLButtonElement, ToggleBaseButtonProps>(
|
|
||||||
function ToggleBaseButton(props, ref) {
|
|
||||||
const toggle = useToggleContext();
|
|
||||||
const { variant, ...rest } = props;
|
|
||||||
return <Button variant={toggle.pressed ? variant.on : variant.off} ref={ref} {...rest} />;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface ToggleBaseButtonProps extends Omit<ButtonProps, "variant"> {
|
||||||
|
variant: Record<"on" | "off", ButtonProps["variant"]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToggleBaseButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
ToggleBaseButtonProps
|
||||||
|
>(function ToggleBaseButton(props, ref) {
|
||||||
|
const toggle = useToggleContext();
|
||||||
|
const { variant, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={toggle.pressed ? variant.on : variant.off}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const ToggleIndicator = ChakraToggle.Indicator;
|
export const ToggleIndicator = ChakraToggle.Indicator;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { Combobox as ChakraCombobox, Portal } from '@chakra-ui/react';
|
import { Combobox as ChakraCombobox, Portal } from "@chakra-ui/react";
|
||||||
import { CloseButton } from '@/components/ui/buttons/close-button';
|
import { CloseButton } from "@/components/ui/buttons/close-button";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
interface ComboboxControlProps extends ChakraCombobox.ControlProps {
|
interface ComboboxControlProps extends ChakraCombobox.ControlProps {
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComboboxControl = React.forwardRef<HTMLDivElement, ComboboxControlProps>(
|
export const ComboboxControl = React.forwardRef<
|
||||||
function ComboboxControl(props, ref) {
|
HTMLDivElement,
|
||||||
|
ComboboxControlProps
|
||||||
|
>(function ComboboxControl(props, ref) {
|
||||||
const { children, clearable, ...rest } = props;
|
const { children, clearable, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraCombobox.Control {...rest} ref={ref}>
|
<ChakraCombobox.Control {...rest} ref={ref}>
|
||||||
@@ -20,26 +22,34 @@ export const ComboboxControl = React.forwardRef<HTMLDivElement, ComboboxControlP
|
|||||||
</ChakraCombobox.IndicatorGroup>
|
</ChakraCombobox.IndicatorGroup>
|
||||||
</ChakraCombobox.Control>
|
</ChakraCombobox.Control>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const ComboboxClearTrigger = React.forwardRef<HTMLButtonElement, ChakraCombobox.ClearTriggerProps>(
|
const ComboboxClearTrigger = React.forwardRef<
|
||||||
function ComboboxClearTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ChakraCombobox.ClearTriggerProps
|
||||||
|
>(function ComboboxClearTrigger(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraCombobox.ClearTrigger asChild {...props} ref={ref}>
|
<ChakraCombobox.ClearTrigger asChild {...props} ref={ref}>
|
||||||
<CloseButton size='xs' variant='plain' focusVisibleRing='inside' focusRingWidth='2px' pointerEvents='auto' />
|
<CloseButton
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
focusVisibleRing="inside"
|
||||||
|
focusRingWidth="2px"
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
</ChakraCombobox.ClearTrigger>
|
</ChakraCombobox.ClearTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface ComboboxContentProps extends ChakraCombobox.ContentProps {
|
interface ComboboxContentProps extends ChakraCombobox.ContentProps {
|
||||||
portalled?: boolean;
|
portalled?: boolean;
|
||||||
portalRef?: React.RefObject<HTMLElement | null>;
|
portalRef?: React.RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComboboxContent = React.forwardRef<HTMLDivElement, ComboboxContentProps>(
|
export const ComboboxContent = React.forwardRef<
|
||||||
function ComboboxContent(props, ref) {
|
HTMLDivElement,
|
||||||
|
ComboboxContentProps
|
||||||
|
>(function ComboboxContent(props, ref) {
|
||||||
const { portalled = true, portalRef, ...rest } = props;
|
const { portalled = true, portalRef, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Portal disabled={!portalled} container={portalRef}>
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
@@ -48,11 +58,12 @@ export const ComboboxContent = React.forwardRef<HTMLDivElement, ComboboxContentP
|
|||||||
</ChakraCombobox.Positioner>
|
</ChakraCombobox.Positioner>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ComboboxItem = React.forwardRef<HTMLDivElement, ChakraCombobox.ItemProps>(
|
export const ComboboxItem = React.forwardRef<
|
||||||
function ComboboxItem(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraCombobox.ItemProps
|
||||||
|
>(function ComboboxItem(props, ref) {
|
||||||
const { item, children, ...rest } = props;
|
const { item, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraCombobox.Item key={item.value} item={item} {...rest} ref={ref}>
|
<ChakraCombobox.Item key={item.value} item={item} {...rest} ref={ref}>
|
||||||
@@ -60,21 +71,29 @@ export const ComboboxItem = React.forwardRef<HTMLDivElement, ChakraCombobox.Item
|
|||||||
<ChakraCombobox.ItemIndicator />
|
<ChakraCombobox.ItemIndicator />
|
||||||
</ChakraCombobox.Item>
|
</ChakraCombobox.Item>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ComboboxRoot = React.forwardRef<HTMLDivElement, ChakraCombobox.RootProps>(
|
export const ComboboxRoot = React.forwardRef<
|
||||||
function ComboboxRoot(props, ref) {
|
HTMLDivElement,
|
||||||
return <ChakraCombobox.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }} />;
|
ChakraCombobox.RootProps
|
||||||
},
|
>(function ComboboxRoot(props, ref) {
|
||||||
) as ChakraCombobox.RootComponent;
|
return (
|
||||||
|
<ChakraCombobox.Root
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
positioning={{ sameWidth: true, ...props.positioning }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}) as ChakraCombobox.RootComponent;
|
||||||
|
|
||||||
interface ComboboxItemGroupProps extends ChakraCombobox.ItemGroupProps {
|
interface ComboboxItemGroupProps extends ChakraCombobox.ItemGroupProps {
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComboboxItemGroup = React.forwardRef<HTMLDivElement, ComboboxItemGroupProps>(
|
export const ComboboxItemGroup = React.forwardRef<
|
||||||
function ComboboxItemGroup(props, ref) {
|
HTMLDivElement,
|
||||||
|
ComboboxItemGroupProps
|
||||||
|
>(function ComboboxItemGroup(props, ref) {
|
||||||
const { children, label, ...rest } = props;
|
const { children, label, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraCombobox.ItemGroup {...rest} ref={ref}>
|
<ChakraCombobox.ItemGroup {...rest} ref={ref}>
|
||||||
@@ -82,8 +101,7 @@ export const ComboboxItemGroup = React.forwardRef<HTMLDivElement, ComboboxItemGr
|
|||||||
{children}
|
{children}
|
||||||
</ChakraCombobox.ItemGroup>
|
</ChakraCombobox.ItemGroup>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ComboboxLabel = ChakraCombobox.Label;
|
export const ComboboxLabel = ChakraCombobox.Label;
|
||||||
export const ComboboxInput = ChakraCombobox.Input;
|
export const ComboboxInput = ChakraCombobox.Input;
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { Listbox as ChakraListbox } from '@chakra-ui/react';
|
import { Listbox as ChakraListbox } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export const ListboxRoot = React.forwardRef<HTMLDivElement, ChakraListbox.RootProps>(function ListboxRoot(props, ref) {
|
export const ListboxRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraListbox.RootProps
|
||||||
|
>(function ListboxRoot(props, ref) {
|
||||||
return <ChakraListbox.Root {...props} ref={ref} />;
|
return <ChakraListbox.Root {...props} ref={ref} />;
|
||||||
}) as ChakraListbox.RootComponent;
|
}) as ChakraListbox.RootComponent;
|
||||||
|
|
||||||
export const ListboxContent = React.forwardRef<HTMLDivElement, ChakraListbox.ContentProps>(
|
export const ListboxContent = React.forwardRef<
|
||||||
function ListboxContent(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraListbox.ContentProps
|
||||||
|
>(function ListboxContent(props, ref) {
|
||||||
return <ChakraListbox.Content {...props} ref={ref} />;
|
return <ChakraListbox.Content {...props} ref={ref} />;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ListboxItem = React.forwardRef<HTMLDivElement, ChakraListbox.ItemProps>(function ListboxItem(props, ref) {
|
export const ListboxItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraListbox.ItemProps
|
||||||
|
>(function ListboxItem(props, ref) {
|
||||||
const { children, ...rest } = props;
|
const { children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraListbox.Item {...rest} ref={ref}>
|
<ChakraListbox.Item {...rest} ref={ref}>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { CollectionItem } from '@chakra-ui/react';
|
import type { CollectionItem } from "@chakra-ui/react";
|
||||||
import { Select as ChakraSelect, Portal } from '@chakra-ui/react';
|
import { Select as ChakraSelect, Portal } from "@chakra-ui/react";
|
||||||
import { CloseButton } from '../buttons/close-button';
|
import { CloseButton } from "../buttons/close-button";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
interface SelectTriggerProps extends ChakraSelect.ControlProps {
|
interface SelectTriggerProps extends ChakraSelect.ControlProps {
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
|
export const SelectTrigger = React.forwardRef<
|
||||||
function SelectTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
|
SelectTriggerProps
|
||||||
|
>(function SelectTrigger(props, ref) {
|
||||||
const { children, clearable, ...rest } = props;
|
const { children, clearable, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.Control {...rest}>
|
<ChakraSelect.Control {...rest}>
|
||||||
@@ -21,25 +23,34 @@ export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerPr
|
|||||||
</ChakraSelect.IndicatorGroup>
|
</ChakraSelect.IndicatorGroup>
|
||||||
</ChakraSelect.Control>
|
</ChakraSelect.Control>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(
|
const SelectClearTrigger = React.forwardRef<
|
||||||
function SelectClearTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ChakraSelect.ClearTriggerProps
|
||||||
|
>(function SelectClearTrigger(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
|
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
|
||||||
<CloseButton size='xs' variant='plain' focusVisibleRing='inside' focusRingWidth='2px' pointerEvents='auto' />
|
<CloseButton
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
focusVisibleRing="inside"
|
||||||
|
focusRingWidth="2px"
|
||||||
|
pointerEvents="auto"
|
||||||
|
/>
|
||||||
</ChakraSelect.ClearTrigger>
|
</ChakraSelect.ClearTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface SelectContentProps extends ChakraSelect.ContentProps {
|
interface SelectContentProps extends ChakraSelect.ContentProps {
|
||||||
portalled?: boolean;
|
portalled?: boolean;
|
||||||
portalRef?: React.RefObject<HTMLElement | null>;
|
portalRef?: React.RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(function SelectContent(props, ref) {
|
export const SelectContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
SelectContentProps
|
||||||
|
>(function SelectContent(props, ref) {
|
||||||
const { portalled = true, portalRef, ...rest } = props;
|
const { portalled = true, portalRef, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Portal disabled={!portalled} container={portalRef}>
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
@@ -50,7 +61,10 @@ export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProps>(function SelectItem(props, ref) {
|
export const SelectItem = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraSelect.ItemProps
|
||||||
|
>(function SelectItem(props, ref) {
|
||||||
const { item, children, ...rest } = props;
|
const { item, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
|
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
|
||||||
@@ -60,12 +74,17 @@ export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProp
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
|
interface SelectValueTextProps extends Omit<
|
||||||
|
ChakraSelect.ValueTextProps,
|
||||||
|
"children"
|
||||||
|
> {
|
||||||
children?(items: CollectionItem[]): React.ReactNode;
|
children?(items: CollectionItem[]): React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(
|
export const SelectValueText = React.forwardRef<
|
||||||
function SelectValueText(props, ref) {
|
HTMLSpanElement,
|
||||||
|
SelectValueTextProps
|
||||||
|
>(function SelectValueText(props, ref) {
|
||||||
const { children, ...rest } = props;
|
const { children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.ValueText {...rest} ref={ref}>
|
<ChakraSelect.ValueText {...rest} ref={ref}>
|
||||||
@@ -74,18 +93,25 @@ export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueText
|
|||||||
const items = select.selectedItems;
|
const items = select.selectedItems;
|
||||||
if (items.length === 0) return props.placeholder;
|
if (items.length === 0) return props.placeholder;
|
||||||
if (children) return children(items);
|
if (children) return children(items);
|
||||||
if (items.length === 1) return select.collection.stringifyItem(items[0]);
|
if (items.length === 1)
|
||||||
|
return select.collection.stringifyItem(items[0]);
|
||||||
return `${items.length} selected`;
|
return `${items.length} selected`;
|
||||||
}}
|
}}
|
||||||
</ChakraSelect.Context>
|
</ChakraSelect.Context>
|
||||||
</ChakraSelect.ValueText>
|
</ChakraSelect.ValueText>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const SelectRoot = React.forwardRef<HTMLDivElement, ChakraSelect.RootProps>(function SelectRoot(props, ref) {
|
export const SelectRoot = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraSelect.RootProps
|
||||||
|
>(function SelectRoot(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }}>
|
<ChakraSelect.Root
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
positioning={{ sameWidth: true, ...props.positioning }}
|
||||||
|
>
|
||||||
{props.asChild ? (
|
{props.asChild ? (
|
||||||
props.children
|
props.children
|
||||||
) : (
|
) : (
|
||||||
@@ -102,8 +128,10 @@ interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
|
|||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(
|
export const SelectItemGroup = React.forwardRef<
|
||||||
function SelectItemGroup(props, ref) {
|
HTMLDivElement,
|
||||||
|
SelectItemGroupProps
|
||||||
|
>(function SelectItemGroup(props, ref) {
|
||||||
const { children, label, ...rest } = props;
|
const { children, label, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraSelect.ItemGroup {...rest} ref={ref}>
|
<ChakraSelect.ItemGroup {...rest} ref={ref}>
|
||||||
@@ -111,8 +139,7 @@ export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupP
|
|||||||
{children}
|
{children}
|
||||||
</ChakraSelect.ItemGroup>
|
</ChakraSelect.ItemGroup>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const SelectLabel = ChakraSelect.Label;
|
export const SelectLabel = ChakraSelect.Label;
|
||||||
export const SelectItemText = ChakraSelect.ItemText;
|
export const SelectItemText = ChakraSelect.ItemText;
|
||||||
|
|||||||
@@ -1,38 +1,44 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { TreeView as ChakraTreeView } from '@chakra-ui/react';
|
import { TreeView as ChakraTreeView } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export const TreeViewRoot = React.forwardRef<HTMLDivElement, ChakraTreeView.RootProps>(
|
export const TreeViewRoot = React.forwardRef<
|
||||||
function TreeViewRoot(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraTreeView.RootProps
|
||||||
|
>(function TreeViewRoot(props, ref) {
|
||||||
return <ChakraTreeView.Root {...props} ref={ref} />;
|
return <ChakraTreeView.Root {...props} ref={ref} />;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface TreeViewTreeProps extends ChakraTreeView.TreeProps {}
|
interface TreeViewTreeProps extends ChakraTreeView.TreeProps {}
|
||||||
|
|
||||||
export const TreeViewTree = React.forwardRef<HTMLDivElement, TreeViewTreeProps>(function TreeViewTree(props, ref) {
|
export const TreeViewTree = React.forwardRef<HTMLDivElement, TreeViewTreeProps>(
|
||||||
|
function TreeViewTree(props, ref) {
|
||||||
const { ...rest } = props;
|
const { ...rest } = props;
|
||||||
return <ChakraTreeView.Tree {...rest} ref={ref} />;
|
return <ChakraTreeView.Tree {...rest} ref={ref} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TreeViewBranch = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraTreeView.BranchProps
|
||||||
|
>(function TreeViewBranch(props, ref) {
|
||||||
|
return <ChakraTreeView.Branch {...props} ref={ref} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TreeViewBranch = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchProps>(
|
export const TreeViewBranchControl = React.forwardRef<
|
||||||
function TreeViewBranch(props, ref) {
|
HTMLDivElement,
|
||||||
return <ChakraTreeView.Branch {...props} ref={ref} />;
|
ChakraTreeView.BranchControlProps
|
||||||
},
|
>(function TreeViewBranchControl(props, ref) {
|
||||||
);
|
|
||||||
|
|
||||||
export const TreeViewBranchControl = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchControlProps>(
|
|
||||||
function TreeViewBranchControl(props, ref) {
|
|
||||||
return <ChakraTreeView.BranchControl {...props} ref={ref} />;
|
return <ChakraTreeView.BranchControl {...props} ref={ref} />;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const TreeViewItem = React.forwardRef<HTMLDivElement, ChakraTreeView.ItemProps>(
|
export const TreeViewItem = React.forwardRef<
|
||||||
function TreeViewItem(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraTreeView.ItemProps
|
||||||
|
>(function TreeViewItem(props, ref) {
|
||||||
return <ChakraTreeView.Item {...props} ref={ref} />;
|
return <ChakraTreeView.Item {...props} ref={ref} />;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const TreeViewLabel = ChakraTreeView.Label;
|
export const TreeViewLabel = ChakraTreeView.Label;
|
||||||
export const TreeViewBranchIndicator = ChakraTreeView.BranchIndicator;
|
export const TreeViewBranchIndicator = ChakraTreeView.BranchIndicator;
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { IconButtonProps, SpanProps } from '@chakra-ui/react';
|
import type { IconButtonProps, SpanProps } from "@chakra-ui/react";
|
||||||
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react';
|
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react";
|
||||||
import { ThemeProvider, useTheme } from 'next-themes';
|
import { ThemeProvider, useTheme } from "next-themes";
|
||||||
import type { ThemeProviderProps } from 'next-themes';
|
import type { ThemeProviderProps } from "next-themes";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LuMoon, LuSun } from 'react-icons/lu';
|
import { LuMoon, LuSun } from "react-icons/lu";
|
||||||
|
|
||||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||||
|
|
||||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||||
return <ThemeProvider attribute='class' disableTransitionOnChange {...props} />;
|
return (
|
||||||
|
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ColorMode = 'light' | 'dark';
|
export type ColorMode = "light" | "dark";
|
||||||
|
|
||||||
export interface UseColorModeReturn {
|
export interface UseColorModeReturn {
|
||||||
colorMode: ColorMode;
|
colorMode: ColorMode;
|
||||||
@@ -25,7 +27,7 @@ export function useColorMode(): UseColorModeReturn {
|
|||||||
const { resolvedTheme, setTheme, forcedTheme } = useTheme();
|
const { resolvedTheme, setTheme, forcedTheme } = useTheme();
|
||||||
const colorMode = forcedTheme || resolvedTheme;
|
const colorMode = forcedTheme || resolvedTheme;
|
||||||
const toggleColorMode = () => {
|
const toggleColorMode = () => {
|
||||||
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
colorMode: colorMode as ColorMode,
|
colorMode: colorMode as ColorMode,
|
||||||
@@ -43,32 +45,34 @@ export function useColorModeValue<T>(light: T, dark: T) {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return light;
|
return light;
|
||||||
}
|
}
|
||||||
return colorMode === 'dark' ? dark : light;
|
return colorMode === "dark" ? dark : light;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ColorModeIcon() {
|
export function ColorModeIcon() {
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
|
return colorMode === "dark" ? <LuMoon /> : <LuSun />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColorModeButtonProps extends Omit<IconButtonProps, 'aria-label'> {}
|
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||||
|
|
||||||
export const ColorModeButton = React.forwardRef<HTMLButtonElement, ColorModeButtonProps>(
|
export const ColorModeButton = React.forwardRef<
|
||||||
function ColorModeButton(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ColorModeButtonProps
|
||||||
|
>(function ColorModeButton(props, ref) {
|
||||||
const { toggleColorMode } = useColorMode();
|
const { toggleColorMode } = useColorMode();
|
||||||
return (
|
return (
|
||||||
<ClientOnly fallback={<Skeleton boxSize='9' />}>
|
<ClientOnly fallback={<Skeleton boxSize="9" />}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={toggleColorMode}
|
onClick={toggleColorMode}
|
||||||
variant='ghost'
|
variant="ghost"
|
||||||
aria-label='Toggle color mode'
|
aria-label="Toggle color mode"
|
||||||
size='sm'
|
size="sm"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
css={{
|
css={{
|
||||||
_icon: {
|
_icon: {
|
||||||
width: '5',
|
width: "5",
|
||||||
height: '5',
|
height: "5",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -76,33 +80,36 @@ export const ColorModeButton = React.forwardRef<HTMLButtonElement, ColorModeButt
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function LightMode(props, ref) {
|
||||||
|
return (
|
||||||
|
<Span
|
||||||
|
color="fg"
|
||||||
|
display="contents"
|
||||||
|
className="chakra-theme light"
|
||||||
|
colorPalette="gray"
|
||||||
|
colorScheme="light"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(function LightMode(props, ref) {
|
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||||
|
function DarkMode(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Span
|
<Span
|
||||||
color='fg'
|
color="fg"
|
||||||
display='contents'
|
display="contents"
|
||||||
className='chakra-theme light'
|
className="chakra-theme dark"
|
||||||
colorPalette='gray'
|
colorPalette="gray"
|
||||||
colorScheme='light'
|
colorScheme="dark"
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(function DarkMode(props, ref) {
|
|
||||||
return (
|
|
||||||
<Span
|
|
||||||
color='fg'
|
|
||||||
display='contents'
|
|
||||||
className='chakra-theme dark'
|
|
||||||
colorPalette='gray'
|
|
||||||
colorScheme='dark'
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Avatar as ChakraAvatar, AvatarGroup as ChakraAvatarGroup } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
Avatar as ChakraAvatar,
|
||||||
|
AvatarGroup as ChakraAvatarGroup,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>;
|
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>;
|
||||||
|
|
||||||
@@ -7,20 +10,25 @@ export interface AvatarProps extends ChakraAvatar.RootProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
src?: string;
|
src?: string;
|
||||||
srcSet?: string;
|
srcSet?: string;
|
||||||
loading?: ImageProps['loading'];
|
loading?: ImageProps["loading"];
|
||||||
icon?: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(function Avatar(props, ref) {
|
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||||
const { name, src, srcSet, loading, icon, fallback, children, ...rest } = props;
|
function Avatar(props, ref) {
|
||||||
|
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||||
|
props;
|
||||||
return (
|
return (
|
||||||
<ChakraAvatar.Root ref={ref} {...rest}>
|
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||||
<ChakraAvatar.Fallback name={name}>{icon || fallback}</ChakraAvatar.Fallback>
|
<ChakraAvatar.Fallback name={name}>
|
||||||
|
{icon || fallback}
|
||||||
|
</ChakraAvatar.Fallback>
|
||||||
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||||
{children}
|
{children}
|
||||||
</ChakraAvatar.Root>
|
</ChakraAvatar.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const AvatarGroup = ChakraAvatarGroup;
|
export const AvatarGroup = ChakraAvatarGroup;
|
||||||
|
|||||||
@@ -1,70 +1,99 @@
|
|||||||
import type { ButtonProps, InputProps } from '@chakra-ui/react';
|
import type { ButtonProps, InputProps } from "@chakra-ui/react";
|
||||||
import { Button, Clipboard as ChakraClipboard, IconButton, Input } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
Button,
|
||||||
import { LuCheck, LuClipboard, LuLink } from 'react-icons/lu';
|
Clipboard as ChakraClipboard,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { LuCheck, LuClipboard, LuLink } from "react-icons/lu";
|
||||||
|
|
||||||
const ClipboardIcon = React.forwardRef<HTMLDivElement, ChakraClipboard.IndicatorProps>(
|
const ClipboardIcon = React.forwardRef<
|
||||||
function ClipboardIcon(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraClipboard.IndicatorProps
|
||||||
|
>(function ClipboardIcon(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Indicator copied={<LuCheck />} {...props} ref={ref}>
|
<ChakraClipboard.Indicator copied={<LuCheck />} {...props} ref={ref}>
|
||||||
<LuClipboard />
|
<LuClipboard />
|
||||||
</ChakraClipboard.Indicator>
|
</ChakraClipboard.Indicator>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const ClipboardCopyText = React.forwardRef<HTMLDivElement, ChakraClipboard.IndicatorProps>(
|
const ClipboardCopyText = React.forwardRef<
|
||||||
function ClipboardCopyText(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraClipboard.IndicatorProps
|
||||||
|
>(function ClipboardCopyText(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Indicator copied='Copied' {...props} ref={ref}>
|
<ChakraClipboard.Indicator copied="Copied" {...props} ref={ref}>
|
||||||
Copy
|
Copy
|
||||||
</ChakraClipboard.Indicator>
|
</ChakraClipboard.Indicator>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ClipboardLabel = React.forwardRef<HTMLLabelElement, ChakraClipboard.LabelProps>(
|
export const ClipboardLabel = React.forwardRef<
|
||||||
function ClipboardLabel(props, ref) {
|
HTMLLabelElement,
|
||||||
|
ChakraClipboard.LabelProps
|
||||||
|
>(function ClipboardLabel(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Label textStyle='sm' fontWeight='medium' display='inline-block' mb='1' {...props} ref={ref} />
|
<ChakraClipboard.Label
|
||||||
|
textStyle="sm"
|
||||||
|
fontWeight="medium"
|
||||||
|
display="inline-block"
|
||||||
|
mb="1"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ClipboardButton = React.forwardRef<HTMLButtonElement, ButtonProps>(function ClipboardButton(props, ref) {
|
export const ClipboardButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function ClipboardButton(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Trigger asChild>
|
<ChakraClipboard.Trigger asChild>
|
||||||
<Button ref={ref} size='sm' variant='surface' {...props}>
|
<Button ref={ref} size="sm" variant="surface" {...props}>
|
||||||
<ClipboardIcon />
|
<ClipboardIcon />
|
||||||
<ClipboardCopyText />
|
<ClipboardCopyText />
|
||||||
</Button>
|
</Button>
|
||||||
</ChakraClipboard.Trigger>
|
</ChakraClipboard.Trigger>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const ClipboardLink = React.forwardRef<HTMLButtonElement, ButtonProps>(function ClipboardLink(props, ref) {
|
export const ClipboardLink = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
function ClipboardLink(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Trigger asChild>
|
<ChakraClipboard.Trigger asChild>
|
||||||
<Button unstyled variant='plain' size='xs' display='inline-flex' alignItems='center' gap='2' ref={ref} {...props}>
|
<Button
|
||||||
|
unstyled
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
display="inline-flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap="2"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<LuLink />
|
<LuLink />
|
||||||
<ClipboardCopyText />
|
<ClipboardCopyText />
|
||||||
</Button>
|
</Button>
|
||||||
</ChakraClipboard.Trigger>
|
</ChakraClipboard.Trigger>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const ClipboardIconButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export const ClipboardIconButton = React.forwardRef<
|
||||||
function ClipboardIconButton(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ButtonProps
|
||||||
|
>(function ClipboardIconButton(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraClipboard.Trigger asChild>
|
<ChakraClipboard.Trigger asChild>
|
||||||
<IconButton ref={ref} size='xs' variant='subtle' {...props}>
|
<IconButton ref={ref} size="xs" variant="subtle" {...props}>
|
||||||
<ClipboardIcon />
|
<ClipboardIcon />
|
||||||
<ClipboardCopyText srOnly />
|
<ClipboardCopyText srOnly />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ChakraClipboard.Trigger>
|
</ChakraClipboard.Trigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ClipboardInput = React.forwardRef<HTMLInputElement, InputProps>(
|
export const ClipboardInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
function ClipboardInputElement(props, ref) {
|
function ClipboardInputElement(props, ref) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataList as ChakraDataList } from '@chakra-ui/react';
|
import { DataList as ChakraDataList } from "@chakra-ui/react";
|
||||||
import { InfoTip } from '@/components/ui/overlays/toggle-tip';
|
import { InfoTip } from "@/components/ui/overlays/toggle-tip";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export const DataListRoot = ChakraDataList.Root;
|
export const DataListRoot = ChakraDataList.Root;
|
||||||
|
|
||||||
@@ -11,16 +11,20 @@ interface ItemProps extends ChakraDataList.ItemProps {
|
|||||||
grow?: boolean;
|
grow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataListItem = React.forwardRef<HTMLDivElement, ItemProps>(function DataListItem(props, ref) {
|
export const DataListItem = React.forwardRef<HTMLDivElement, ItemProps>(
|
||||||
|
function DataListItem(props, ref) {
|
||||||
const { label, info, value, children, grow, ...rest } = props;
|
const { label, info, value, children, grow, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraDataList.Item ref={ref} {...rest}>
|
<ChakraDataList.Item ref={ref} {...rest}>
|
||||||
<ChakraDataList.ItemLabel flex={grow ? '1' : undefined}>
|
<ChakraDataList.ItemLabel flex={grow ? "1" : undefined}>
|
||||||
{label}
|
{label}
|
||||||
{info && <InfoTip>{info}</InfoTip>}
|
{info && <InfoTip>{info}</InfoTip>}
|
||||||
</ChakraDataList.ItemLabel>
|
</ChakraDataList.ItemLabel>
|
||||||
<ChakraDataList.ItemValue flex={grow ? '1' : undefined}>{value}</ChakraDataList.ItemValue>
|
<ChakraDataList.ItemValue flex={grow ? "1" : undefined}>
|
||||||
|
{value}
|
||||||
|
</ChakraDataList.ItemValue>
|
||||||
{children}
|
{children}
|
||||||
</ChakraDataList.Item>
|
</ChakraDataList.Item>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { QrCode as ChakraQrCode } from '@chakra-ui/react';
|
import { QrCode as ChakraQrCode } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface QrCodeProps extends Omit<ChakraQrCode.RootProps, 'fill' | 'overlay'> {
|
export interface QrCodeProps extends Omit<
|
||||||
|
ChakraQrCode.RootProps,
|
||||||
|
"fill" | "overlay"
|
||||||
|
> {
|
||||||
fill?: string;
|
fill?: string;
|
||||||
overlay?: React.ReactNode;
|
overlay?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QrCode = React.forwardRef<HTMLDivElement, QrCodeProps>(function QrCode(props, ref) {
|
export const QrCode = React.forwardRef<HTMLDivElement, QrCodeProps>(
|
||||||
|
function QrCode(props, ref) {
|
||||||
const { children, fill, overlay, ...rest } = props;
|
const { children, fill, overlay, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraQrCode.Root ref={ref} {...rest}>
|
<ChakraQrCode.Root ref={ref} {...rest}>
|
||||||
@@ -17,4 +21,5 @@ export const QrCode = React.forwardRef<HTMLDivElement, QrCodeProps>(function QrC
|
|||||||
{overlay && <ChakraQrCode.Overlay>{overlay}</ChakraQrCode.Overlay>}
|
{overlay && <ChakraQrCode.Overlay>{overlay}</ChakraQrCode.Overlay>}
|
||||||
</ChakraQrCode.Root>
|
</ChakraQrCode.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { Badge, type BadgeProps, Stat as ChakraStat, FormatNumber } from '@chakra-ui/react';
|
import {
|
||||||
import { InfoTip } from '@/components/ui/overlays/toggle-tip';
|
Badge,
|
||||||
import * as React from 'react';
|
type BadgeProps,
|
||||||
|
Stat as ChakraStat,
|
||||||
|
FormatNumber,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { InfoTip } from "@/components/ui/overlays/toggle-tip";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
interface StatLabelProps extends ChakraStat.LabelProps {
|
interface StatLabelProps extends ChakraStat.LabelProps {
|
||||||
info?: React.ReactNode;
|
info?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(function StatLabel(props, ref) {
|
export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(
|
||||||
|
function StatLabel(props, ref) {
|
||||||
const { info, children, ...rest } = props;
|
const { info, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraStat.Label {...rest} ref={ref}>
|
<ChakraStat.Label {...rest} ref={ref}>
|
||||||
@@ -14,39 +20,48 @@ export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(functi
|
|||||||
{info && <InfoTip>{info}</InfoTip>}
|
{info && <InfoTip>{info}</InfoTip>}
|
||||||
</ChakraStat.Label>
|
</ChakraStat.Label>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
interface StatValueTextProps extends ChakraStat.ValueTextProps {
|
interface StatValueTextProps extends ChakraStat.ValueTextProps {
|
||||||
value?: number;
|
value?: number;
|
||||||
formatOptions?: Intl.NumberFormatOptions;
|
formatOptions?: Intl.NumberFormatOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatValueText = React.forwardRef<HTMLDivElement, StatValueTextProps>(function StatValueText(props, ref) {
|
export const StatValueText = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
StatValueTextProps
|
||||||
|
>(function StatValueText(props, ref) {
|
||||||
const { value, formatOptions, children, ...rest } = props;
|
const { value, formatOptions, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraStat.ValueText {...rest} ref={ref}>
|
<ChakraStat.ValueText {...rest} ref={ref}>
|
||||||
{children || (value != null && <FormatNumber value={value} {...formatOptions} />)}
|
{children ||
|
||||||
|
(value != null && <FormatNumber value={value} {...formatOptions} />)}
|
||||||
</ChakraStat.ValueText>
|
</ChakraStat.ValueText>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StatUpTrend = React.forwardRef<HTMLDivElement, BadgeProps>(function StatUpTrend(props, ref) {
|
export const StatUpTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
|
function StatUpTrend(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Badge colorPalette='green' gap='0' {...props} ref={ref}>
|
<Badge colorPalette="green" gap="0" {...props} ref={ref}>
|
||||||
<ChakraStat.UpIndicator />
|
<ChakraStat.UpIndicator />
|
||||||
{props.children}
|
{props.children}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const StatDownTrend = React.forwardRef<HTMLDivElement, BadgeProps>(function StatDownTrend(props, ref) {
|
export const StatDownTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
|
function StatDownTrend(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Badge colorPalette='red' gap='0' {...props} ref={ref}>
|
<Badge colorPalette="red" gap="0" {...props} ref={ref}>
|
||||||
<ChakraStat.DownIndicator />
|
<ChakraStat.DownIndicator />
|
||||||
{props.children}
|
{props.children}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const StatRoot = ChakraStat.Root;
|
export const StatRoot = ChakraStat.Root;
|
||||||
export const StatHelpText = ChakraStat.HelpText;
|
export const StatHelpText = ChakraStat.HelpText;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Tag as ChakraTag } from '@chakra-ui/react';
|
import { Tag as ChakraTag } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface TagProps extends ChakraTag.RootProps {
|
export interface TagProps extends ChakraTag.RootProps {
|
||||||
startElement?: React.ReactNode;
|
startElement?: React.ReactNode;
|
||||||
@@ -8,14 +8,26 @@ export interface TagProps extends ChakraTag.RootProps {
|
|||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(function Tag(props, ref) {
|
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||||
const { startElement, endElement, onClose, closable = !!onClose, children, ...rest } = props;
|
function Tag(props, ref) {
|
||||||
|
const {
|
||||||
|
startElement,
|
||||||
|
endElement,
|
||||||
|
onClose,
|
||||||
|
closable = !!onClose,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChakraTag.Root ref={ref} {...rest}>
|
<ChakraTag.Root ref={ref} {...rest}>
|
||||||
{startElement && <ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>}
|
{startElement && (
|
||||||
|
<ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>
|
||||||
|
)}
|
||||||
<ChakraTag.Label>{children}</ChakraTag.Label>
|
<ChakraTag.Label>{children}</ChakraTag.Label>
|
||||||
{endElement && <ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>}
|
{endElement && (
|
||||||
|
<ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>
|
||||||
|
)}
|
||||||
{closable && (
|
{closable && (
|
||||||
<ChakraTag.EndElement>
|
<ChakraTag.EndElement>
|
||||||
<ChakraTag.CloseTrigger onClick={onClose} />
|
<ChakraTag.CloseTrigger onClick={onClose} />
|
||||||
@@ -23,4 +35,5 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(function Tag(prop
|
|||||||
)}
|
)}
|
||||||
</ChakraTag.Root>
|
</ChakraTag.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Timeline as ChakraTimeline } from '@chakra-ui/react';
|
import { Timeline as ChakraTimeline } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
interface TimelineConnectorProps extends ChakraTimeline.IndicatorProps {
|
interface TimelineConnectorProps extends ChakraTimeline.IndicatorProps {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimelineConnector = React.forwardRef<HTMLDivElement, TimelineConnectorProps>(function TimelineConnector(
|
export const TimelineConnector = React.forwardRef<
|
||||||
{ icon, ...props },
|
HTMLDivElement,
|
||||||
ref,
|
TimelineConnectorProps
|
||||||
) {
|
>(function TimelineConnector({ icon, ...props }, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraTimeline.Connector ref={ref}>
|
<ChakraTimeline.Connector ref={ref}>
|
||||||
<ChakraTimeline.Separator />
|
<ChakraTimeline.Separator />
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
import { Accordion, HStack } from '@chakra-ui/react';
|
import { Accordion, HStack } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LuChevronDown } from 'react-icons/lu';
|
import { LuChevronDown } from "react-icons/lu";
|
||||||
|
|
||||||
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
|
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
|
||||||
indicatorPlacement?: 'start' | 'end';
|
indicatorPlacement?: "start" | "end";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccordionItemTrigger = React.forwardRef<HTMLButtonElement, AccordionItemTriggerProps>(
|
export const AccordionItemTrigger = React.forwardRef<
|
||||||
function AccordionItemTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
const { children, indicatorPlacement = 'end', ...rest } = props;
|
AccordionItemTriggerProps
|
||||||
|
>(function AccordionItemTrigger(props, ref) {
|
||||||
|
const { children, indicatorPlacement = "end", ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Accordion.ItemTrigger {...rest} ref={ref}>
|
<Accordion.ItemTrigger {...rest} ref={ref}>
|
||||||
{indicatorPlacement === 'start' && (
|
{indicatorPlacement === "start" && (
|
||||||
<Accordion.ItemIndicator rotate={{ base: '-90deg', _open: '0deg' }}>
|
<Accordion.ItemIndicator rotate={{ base: "-90deg", _open: "0deg" }}>
|
||||||
<LuChevronDown />
|
<LuChevronDown />
|
||||||
</Accordion.ItemIndicator>
|
</Accordion.ItemIndicator>
|
||||||
)}
|
)}
|
||||||
<HStack gap='4' flex='1' textAlign='start' width='full'>
|
<HStack gap="4" flex="1" textAlign="start" width="full">
|
||||||
{children}
|
{children}
|
||||||
</HStack>
|
</HStack>
|
||||||
{indicatorPlacement === 'end' && (
|
{indicatorPlacement === "end" && (
|
||||||
<Accordion.ItemIndicator>
|
<Accordion.ItemIndicator>
|
||||||
<LuChevronDown />
|
<LuChevronDown />
|
||||||
</Accordion.ItemIndicator>
|
</Accordion.ItemIndicator>
|
||||||
)}
|
)}
|
||||||
</Accordion.ItemTrigger>
|
</Accordion.ItemTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
|
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
|
||||||
|
|
||||||
export const AccordionItemContent = React.forwardRef<HTMLDivElement, AccordionItemContentProps>(
|
export const AccordionItemContent = React.forwardRef<
|
||||||
function AccordionItemContent(props, ref) {
|
HTMLDivElement,
|
||||||
|
AccordionItemContentProps
|
||||||
|
>(function AccordionItemContent(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Accordion.ItemContent>
|
<Accordion.ItemContent>
|
||||||
<Accordion.ItemBody {...props} ref={ref} />
|
<Accordion.ItemBody {...props} ref={ref} />
|
||||||
</Accordion.ItemContent>
|
</Accordion.ItemContent>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const AccordionRoot = Accordion.Root;
|
export const AccordionRoot = Accordion.Root;
|
||||||
export const AccordionItem = Accordion.Item;
|
export const AccordionItem = Accordion.Item;
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Breadcrumb, type SystemStyleObject } from '@chakra-ui/react';
|
import { Breadcrumb, type SystemStyleObject } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
|
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
|
||||||
separator?: React.ReactNode;
|
separator?: React.ReactNode;
|
||||||
separatorGap?: SystemStyleObject['gap'];
|
separatorGap?: SystemStyleObject["gap"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BreadcrumbRoot = React.forwardRef<HTMLDivElement, BreadcrumbRootProps>(
|
export const BreadcrumbRoot = React.forwardRef<
|
||||||
function BreadcrumbRoot(props, ref) {
|
HTMLDivElement,
|
||||||
|
BreadcrumbRootProps
|
||||||
|
>(function BreadcrumbRoot(props, ref) {
|
||||||
const { separator, separatorGap, children, ...rest } = props;
|
const { separator, separatorGap, children, ...rest } = props;
|
||||||
|
|
||||||
const validChildren = React.Children.toArray(children).filter(React.isValidElement);
|
const validChildren = React.Children.toArray(children).filter(
|
||||||
|
React.isValidElement,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumb.Root ref={ref} {...rest}>
|
<Breadcrumb.Root ref={ref} {...rest}>
|
||||||
@@ -20,15 +24,16 @@ export const BreadcrumbRoot = React.forwardRef<HTMLDivElement, BreadcrumbRootPro
|
|||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<Breadcrumb.Item>{child}</Breadcrumb.Item>
|
<Breadcrumb.Item>{child}</Breadcrumb.Item>
|
||||||
{!last && <Breadcrumb.Separator>{separator}</Breadcrumb.Separator>}
|
{!last && (
|
||||||
|
<Breadcrumb.Separator>{separator}</Breadcrumb.Separator>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Breadcrumb.List>
|
</Breadcrumb.List>
|
||||||
</Breadcrumb.Root>
|
</Breadcrumb.Root>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const BreadcrumbLink = Breadcrumb.Link;
|
export const BreadcrumbLink = Breadcrumb.Link;
|
||||||
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink;
|
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { ButtonProps, TextProps } from '@chakra-ui/react';
|
import type { ButtonProps, TextProps } from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Pagination as ChakraPagination,
|
Pagination as ChakraPagination,
|
||||||
@@ -8,67 +8,84 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
createContext,
|
createContext,
|
||||||
usePaginationContext,
|
usePaginationContext,
|
||||||
} from '@chakra-ui/react';
|
} from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { HiChevronLeft, HiChevronRight, HiMiniEllipsisHorizontal } from 'react-icons/hi2';
|
import {
|
||||||
import { LinkButton } from '@/components/ui/buttons/link-button';
|
HiChevronLeft,
|
||||||
|
HiChevronRight,
|
||||||
|
HiMiniEllipsisHorizontal,
|
||||||
|
} from "react-icons/hi2";
|
||||||
|
import { LinkButton } from "@/components/ui/buttons/link-button";
|
||||||
|
|
||||||
interface ButtonVariantMap {
|
interface ButtonVariantMap {
|
||||||
current: ButtonProps['variant'];
|
current: ButtonProps["variant"];
|
||||||
default: ButtonProps['variant'];
|
default: ButtonProps["variant"];
|
||||||
ellipsis: ButtonProps['variant'];
|
ellipsis: ButtonProps["variant"];
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationVariant = 'outline' | 'solid' | 'subtle';
|
type PaginationVariant = "outline" | "solid" | "subtle";
|
||||||
|
|
||||||
interface ButtonVariantContext {
|
interface ButtonVariantContext {
|
||||||
size: ButtonProps['size'];
|
size: ButtonProps["size"];
|
||||||
variantMap: ButtonVariantMap;
|
variantMap: ButtonVariantMap;
|
||||||
getHref?: (page: number) => string;
|
getHref?: (page: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({
|
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({
|
||||||
name: 'RootPropsProvider',
|
name: "RootPropsProvider",
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface PaginationRootProps extends Omit<ChakraPagination.RootProps, 'type'> {
|
export interface PaginationRootProps extends Omit<
|
||||||
size?: ButtonProps['size'];
|
ChakraPagination.RootProps,
|
||||||
|
"type"
|
||||||
|
> {
|
||||||
|
size?: ButtonProps["size"];
|
||||||
variant?: PaginationVariant;
|
variant?: PaginationVariant;
|
||||||
getHref?: (page: number) => string;
|
getHref?: (page: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantMap: Record<PaginationVariant, ButtonVariantMap> = {
|
const variantMap: Record<PaginationVariant, ButtonVariantMap> = {
|
||||||
outline: { default: 'ghost', ellipsis: 'plain', current: 'outline' },
|
outline: { default: "ghost", ellipsis: "plain", current: "outline" },
|
||||||
solid: { default: 'outline', ellipsis: 'outline', current: 'solid' },
|
solid: { default: "outline", ellipsis: "outline", current: "solid" },
|
||||||
subtle: { default: 'ghost', ellipsis: 'plain', current: 'subtle' },
|
subtle: { default: "ghost", ellipsis: "plain", current: "subtle" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PaginationRoot = React.forwardRef<HTMLDivElement, PaginationRootProps>(
|
export const PaginationRoot = React.forwardRef<
|
||||||
function PaginationRoot(props, ref) {
|
HTMLDivElement,
|
||||||
const { size = 'sm', variant = 'outline', getHref, ...rest } = props;
|
PaginationRootProps
|
||||||
|
>(function PaginationRoot(props, ref) {
|
||||||
|
const { size = "sm", variant = "outline", getHref, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<RootPropsProvider value={{ size, variantMap: variantMap[variant], getHref }}>
|
<RootPropsProvider
|
||||||
<ChakraPagination.Root ref={ref} type={getHref ? 'link' : 'button'} {...rest} />
|
value={{ size, variantMap: variantMap[variant], getHref }}
|
||||||
|
>
|
||||||
|
<ChakraPagination.Root
|
||||||
|
ref={ref}
|
||||||
|
type={getHref ? "link" : "button"}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
</RootPropsProvider>
|
</RootPropsProvider>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const PaginationEllipsis = React.forwardRef<HTMLDivElement, ChakraPagination.EllipsisProps>(
|
export const PaginationEllipsis = React.forwardRef<
|
||||||
function PaginationEllipsis(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraPagination.EllipsisProps
|
||||||
|
>(function PaginationEllipsis(props, ref) {
|
||||||
const { size, variantMap } = useRootProps();
|
const { size, variantMap } = useRootProps();
|
||||||
return (
|
return (
|
||||||
<ChakraPagination.Ellipsis ref={ref} {...props} asChild>
|
<ChakraPagination.Ellipsis ref={ref} {...props} asChild>
|
||||||
<Button as='span' variant={variantMap.ellipsis} size={size}>
|
<Button as="span" variant={variantMap.ellipsis} size={size}>
|
||||||
<HiMiniEllipsisHorizontal />
|
<HiMiniEllipsisHorizontal />
|
||||||
</Button>
|
</Button>
|
||||||
</ChakraPagination.Ellipsis>
|
</ChakraPagination.Ellipsis>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const PaginationItem = React.forwardRef<HTMLButtonElement, ChakraPagination.ItemProps>(
|
export const PaginationItem = React.forwardRef<
|
||||||
function PaginationItem(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.ItemProps
|
||||||
|
>(function PaginationItem(props, ref) {
|
||||||
const { page } = usePaginationContext();
|
const { page } = usePaginationContext();
|
||||||
const { size, variantMap, getHref } = useRootProps();
|
const { size, variantMap, getHref } = useRootProps();
|
||||||
|
|
||||||
@@ -90,11 +107,12 @@ export const PaginationItem = React.forwardRef<HTMLButtonElement, ChakraPaginati
|
|||||||
</Button>
|
</Button>
|
||||||
</ChakraPagination.Item>
|
</ChakraPagination.Item>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const PaginationPrevTrigger = React.forwardRef<HTMLButtonElement, ChakraPagination.PrevTriggerProps>(
|
export const PaginationPrevTrigger = React.forwardRef<
|
||||||
function PaginationPrevTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.PrevTriggerProps
|
||||||
|
>(function PaginationPrevTrigger(props, ref) {
|
||||||
const { size, variantMap, getHref } = useRootProps();
|
const { size, variantMap, getHref } = useRootProps();
|
||||||
const { previousPage } = usePaginationContext();
|
const { previousPage } = usePaginationContext();
|
||||||
|
|
||||||
@@ -117,17 +135,22 @@ export const PaginationPrevTrigger = React.forwardRef<HTMLButtonElement, ChakraP
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</ChakraPagination.PrevTrigger>
|
</ChakraPagination.PrevTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const PaginationNextTrigger = React.forwardRef<HTMLButtonElement, ChakraPagination.NextTriggerProps>(
|
export const PaginationNextTrigger = React.forwardRef<
|
||||||
function PaginationNextTrigger(props, ref) {
|
HTMLButtonElement,
|
||||||
|
ChakraPagination.NextTriggerProps
|
||||||
|
>(function PaginationNextTrigger(props, ref) {
|
||||||
const { size, variantMap, getHref } = useRootProps();
|
const { size, variantMap, getHref } = useRootProps();
|
||||||
const { nextPage } = usePaginationContext();
|
const { nextPage } = usePaginationContext();
|
||||||
|
|
||||||
if (getHref) {
|
if (getHref) {
|
||||||
return (
|
return (
|
||||||
<LinkButton href={nextPage != null ? getHref(nextPage) : undefined} variant={variantMap.default} size={size}>
|
<LinkButton
|
||||||
|
href={nextPage != null ? getHref(nextPage) : undefined}
|
||||||
|
variant={variantMap.default}
|
||||||
|
size={size}
|
||||||
|
>
|
||||||
<HiChevronRight />
|
<HiChevronRight />
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
);
|
);
|
||||||
@@ -140,18 +163,22 @@ export const PaginationNextTrigger = React.forwardRef<HTMLButtonElement, ChakraP
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</ChakraPagination.NextTrigger>
|
</ChakraPagination.NextTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
|
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
|
||||||
return (
|
return (
|
||||||
<ChakraPagination.Context>
|
<ChakraPagination.Context>
|
||||||
{({ pages }) =>
|
{({ pages }) =>
|
||||||
pages.map((page, index) => {
|
pages.map((page, index) => {
|
||||||
return page.type === 'ellipsis' ? (
|
return page.type === "ellipsis" ? (
|
||||||
<PaginationEllipsis key={index} index={index} {...props} />
|
<PaginationEllipsis key={index} index={index} {...props} />
|
||||||
) : (
|
) : (
|
||||||
<PaginationItem key={index} type='page' value={page.value} {...props} />
|
<PaginationItem
|
||||||
|
key={index}
|
||||||
|
type="page"
|
||||||
|
value={page.value}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -160,23 +187,24 @@ export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface PageTextProps extends TextProps {
|
interface PageTextProps extends TextProps {
|
||||||
format?: 'short' | 'compact' | 'long';
|
format?: "short" | "compact" | "long";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PaginationPageText = React.forwardRef<HTMLParagraphElement, PageTextProps>(
|
export const PaginationPageText = React.forwardRef<
|
||||||
function PaginationPageText(props, ref) {
|
HTMLParagraphElement,
|
||||||
const { format = 'compact', ...rest } = props;
|
PageTextProps
|
||||||
|
>(function PaginationPageText(props, ref) {
|
||||||
|
const { format = "compact", ...rest } = props;
|
||||||
const { page, totalPages, pageRange, count } = usePaginationContext();
|
const { page, totalPages, pageRange, count } = usePaginationContext();
|
||||||
const content = React.useMemo(() => {
|
const content = React.useMemo(() => {
|
||||||
if (format === 'short') return `${page} / ${totalPages}`;
|
if (format === "short") return `${page} / ${totalPages}`;
|
||||||
if (format === 'compact') return `${page} of ${totalPages}`;
|
if (format === "compact") return `${page} of ${totalPages}`;
|
||||||
return `${pageRange.start + 1} - ${Math.min(pageRange.end, count)} of ${count}`;
|
return `${pageRange.start + 1} - ${Math.min(pageRange.end, count)} of ${count}`;
|
||||||
}, [format, page, totalPages, pageRange, count]);
|
}, [format, page, totalPages, pageRange, count]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text fontWeight='medium' ref={ref} {...rest}>
|
<Text fontWeight="medium" ref={ref} {...rest}>
|
||||||
{content}
|
{content}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
import { Box, Steps as ChakraSteps } from '@chakra-ui/react';
|
import { Box, Steps as ChakraSteps } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LuCheck } from 'react-icons/lu';
|
import { LuCheck } from "react-icons/lu";
|
||||||
|
|
||||||
interface StepInfoProps {
|
interface StepInfoProps {
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StepsItemProps extends Omit<ChakraSteps.ItemProps, 'title'>, StepInfoProps {
|
export interface StepsItemProps
|
||||||
|
extends Omit<ChakraSteps.ItemProps, "title">, StepInfoProps {
|
||||||
completedIcon?: React.ReactNode;
|
completedIcon?: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
disableTrigger?: boolean;
|
disableTrigger?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(function StepsItem(props, ref) {
|
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(
|
||||||
const { title, description, completedIcon, icon, disableTrigger, ...rest } = props;
|
function StepsItem(props, ref) {
|
||||||
|
const { title, description, completedIcon, icon, disableTrigger, ...rest } =
|
||||||
|
props;
|
||||||
return (
|
return (
|
||||||
<ChakraSteps.Item {...rest} ref={ref}>
|
<ChakraSteps.Item {...rest} ref={ref}>
|
||||||
<ChakraSteps.Trigger disabled={disableTrigger}>
|
<ChakraSteps.Trigger disabled={disableTrigger}>
|
||||||
<ChakraSteps.Indicator>
|
<ChakraSteps.Indicator>
|
||||||
<ChakraSteps.Status complete={completedIcon || <LuCheck />} incomplete={icon || <ChakraSteps.Number />} />
|
<ChakraSteps.Status
|
||||||
|
complete={completedIcon || <LuCheck />}
|
||||||
|
incomplete={icon || <ChakraSteps.Number />}
|
||||||
|
/>
|
||||||
</ChakraSteps.Indicator>
|
</ChakraSteps.Indicator>
|
||||||
<StepInfo title={title} description={description} />
|
<StepInfo title={title} description={description} />
|
||||||
</ChakraSteps.Trigger>
|
</ChakraSteps.Trigger>
|
||||||
<ChakraSteps.Separator />
|
<ChakraSteps.Separator />
|
||||||
</ChakraSteps.Item>
|
</ChakraSteps.Item>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const StepInfo = (props: StepInfoProps) => {
|
const StepInfo = (props: StepInfoProps) => {
|
||||||
const { title, description } = props;
|
const { title, description } = props;
|
||||||
@@ -43,7 +50,9 @@ const StepInfo = (props: StepInfoProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{title && <ChakraSteps.Title>{title}</ChakraSteps.Title>}
|
{title && <ChakraSteps.Title>{title}</ChakraSteps.Title>}
|
||||||
{description && <ChakraSteps.Description>{description}</ChakraSteps.Description>}
|
{description && (
|
||||||
|
<ChakraSteps.Description>{description}</ChakraSteps.Description>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -53,16 +62,17 @@ interface StepsIndicatorProps {
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StepsIndicator = React.forwardRef<HTMLDivElement, StepsIndicatorProps>(
|
export const StepsIndicator = React.forwardRef<
|
||||||
function StepsIndicator(props, ref) {
|
HTMLDivElement,
|
||||||
|
StepsIndicatorProps
|
||||||
|
>(function StepsIndicator(props, ref) {
|
||||||
const { icon = <ChakraSteps.Number />, completedIcon } = props;
|
const { icon = <ChakraSteps.Number />, completedIcon } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraSteps.Indicator ref={ref}>
|
<ChakraSteps.Indicator ref={ref}>
|
||||||
<ChakraSteps.Status complete={completedIcon} incomplete={icon} />
|
<ChakraSteps.Status complete={completedIcon} incomplete={icon} />
|
||||||
</ChakraSteps.Indicator>
|
</ChakraSteps.Indicator>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const StepsList = ChakraSteps.List;
|
export const StepsList = ChakraSteps.List;
|
||||||
export const StepsRoot = ChakraSteps.Root;
|
export const StepsRoot = ChakraSteps.Root;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Alert as ChakraAlert } from '@chakra-ui/react';
|
import { Alert as ChakraAlert } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> {
|
export interface AlertProps extends Omit<ChakraAlert.RootProps, "title"> {
|
||||||
startElement?: React.ReactNode;
|
startElement?: React.ReactNode;
|
||||||
endElement?: React.ReactNode;
|
endElement?: React.ReactNode;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
icon?: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(props, ref) {
|
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||||
|
function Alert(props, ref) {
|
||||||
const { title, children, icon, startElement, endElement, ...rest } = props;
|
const { title, children, icon, startElement, endElement, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraAlert.Root ref={ref} {...rest}>
|
<ChakraAlert.Root ref={ref} {...rest}>
|
||||||
@@ -19,9 +20,10 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert
|
|||||||
<ChakraAlert.Description>{children}</ChakraAlert.Description>
|
<ChakraAlert.Description>{children}</ChakraAlert.Description>
|
||||||
</ChakraAlert.Content>
|
</ChakraAlert.Content>
|
||||||
) : (
|
) : (
|
||||||
<ChakraAlert.Title flex='1'>{title}</ChakraAlert.Title>
|
<ChakraAlert.Title flex="1">{title}</ChakraAlert.Title>
|
||||||
)}
|
)}
|
||||||
{endElement}
|
{endElement}
|
||||||
</ChakraAlert.Root>
|
</ChakraAlert.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EmptyState as ChakraEmptyState, VStack } from '@chakra-ui/react';
|
import { EmptyState as ChakraEmptyState, VStack } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
|
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -7,16 +7,21 @@ export interface EmptyStateProps extends ChakraEmptyState.RootProps {
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(props, ref) {
|
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
||||||
|
function EmptyState(props, ref) {
|
||||||
const { title, description, icon, children, ...rest } = props;
|
const { title, description, icon, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraEmptyState.Root ref={ref} {...rest}>
|
<ChakraEmptyState.Root ref={ref} {...rest}>
|
||||||
<ChakraEmptyState.Content>
|
<ChakraEmptyState.Content>
|
||||||
{icon && <ChakraEmptyState.Indicator>{icon}</ChakraEmptyState.Indicator>}
|
{icon && (
|
||||||
|
<ChakraEmptyState.Indicator>{icon}</ChakraEmptyState.Indicator>
|
||||||
|
)}
|
||||||
{description ? (
|
{description ? (
|
||||||
<VStack textAlign='center'>
|
<VStack textAlign="center">
|
||||||
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||||
<ChakraEmptyState.Description>{description}</ChakraEmptyState.Description>
|
<ChakraEmptyState.Description>
|
||||||
|
{description}
|
||||||
|
</ChakraEmptyState.Description>
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||||
@@ -25,4 +30,5 @@ export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(func
|
|||||||
</ChakraEmptyState.Content>
|
</ChakraEmptyState.Content>
|
||||||
</ChakraEmptyState.Root>
|
</ChakraEmptyState.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import type { SystemStyleObject } from '@chakra-ui/react';
|
import type { SystemStyleObject } from "@chakra-ui/react";
|
||||||
import { AbsoluteCenter, ProgressCircle as ChakraProgressCircle } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
AbsoluteCenter,
|
||||||
|
ProgressCircle as ChakraProgressCircle,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps {
|
interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps {
|
||||||
trackColor?: SystemStyleObject['stroke'];
|
trackColor?: SystemStyleObject["stroke"];
|
||||||
cap?: SystemStyleObject['strokeLinecap'];
|
cap?: SystemStyleObject["strokeLinecap"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProgressCircleRing = React.forwardRef<SVGSVGElement, ProgressCircleRingProps>(
|
export const ProgressCircleRing = React.forwardRef<
|
||||||
function ProgressCircleRing(props, ref) {
|
SVGSVGElement,
|
||||||
|
ProgressCircleRingProps
|
||||||
|
>(function ProgressCircleRing(props, ref) {
|
||||||
const { trackColor, cap, color, ...rest } = props;
|
const { trackColor, cap, color, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraProgressCircle.Circle {...rest} ref={ref}>
|
<ChakraProgressCircle.Circle {...rest} ref={ref}>
|
||||||
@@ -16,17 +21,17 @@ export const ProgressCircleRing = React.forwardRef<SVGSVGElement, ProgressCircle
|
|||||||
<ChakraProgressCircle.Range stroke={color} strokeLinecap={cap} />
|
<ChakraProgressCircle.Range stroke={color} strokeLinecap={cap} />
|
||||||
</ChakraProgressCircle.Circle>
|
</ChakraProgressCircle.Circle>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ProgressCircleValueText = React.forwardRef<HTMLDivElement, ChakraProgressCircle.ValueTextProps>(
|
export const ProgressCircleValueText = React.forwardRef<
|
||||||
function ProgressCircleValueText(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraProgressCircle.ValueTextProps
|
||||||
|
>(function ProgressCircleValueText(props, ref) {
|
||||||
return (
|
return (
|
||||||
<AbsoluteCenter>
|
<AbsoluteCenter>
|
||||||
<ChakraProgressCircle.ValueText {...props} ref={ref} />
|
<ChakraProgressCircle.ValueText {...props} ref={ref} />
|
||||||
</AbsoluteCenter>
|
</AbsoluteCenter>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ProgressCircleRoot = ChakraProgressCircle.Root;
|
export const ProgressCircleRoot = ChakraProgressCircle.Root;
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { Progress as ChakraProgress } from '@chakra-ui/react';
|
import { Progress as ChakraProgress } from "@chakra-ui/react";
|
||||||
import { InfoTip } from '../overlays/toggle-tip';
|
import { InfoTip } from "../overlays/toggle-tip";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export const ProgressBar = React.forwardRef<HTMLDivElement, ChakraProgress.TrackProps>(
|
export const ProgressBar = React.forwardRef<
|
||||||
function ProgressBar(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraProgress.TrackProps
|
||||||
|
>(function ProgressBar(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraProgress.Track {...props} ref={ref}>
|
<ChakraProgress.Track {...props} ref={ref}>
|
||||||
<ChakraProgress.Range />
|
<ChakraProgress.Range />
|
||||||
</ChakraProgress.Track>
|
</ChakraProgress.Track>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export interface ProgressLabelProps extends ChakraProgress.LabelProps {
|
export interface ProgressLabelProps extends ChakraProgress.LabelProps {
|
||||||
info?: React.ReactNode;
|
info?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProgressLabel = React.forwardRef<HTMLDivElement, ProgressLabelProps>(function ProgressLabel(props, ref) {
|
export const ProgressLabel = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ProgressLabelProps
|
||||||
|
>(function ProgressLabel(props, ref) {
|
||||||
const { children, info, ...rest } = props;
|
const { children, info, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraProgress.Label {...rest} ref={ref}>
|
<ChakraProgress.Label {...rest} ref={ref}>
|
||||||
|
|||||||
@@ -1,35 +1,47 @@
|
|||||||
import type { SkeletonProps as ChakraSkeletonProps, CircleProps } from '@chakra-ui/react';
|
import type {
|
||||||
import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react';
|
SkeletonProps as ChakraSkeletonProps,
|
||||||
import * as React from 'react';
|
CircleProps,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { Skeleton as ChakraSkeleton, Circle, Stack } from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
export interface SkeletonCircleProps extends ChakraSkeletonProps {
|
export interface SkeletonCircleProps extends ChakraSkeletonProps {
|
||||||
size?: CircleProps['size'];
|
size?: CircleProps["size"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SkeletonCircle = React.forwardRef<HTMLDivElement, SkeletonCircleProps>(
|
export const SkeletonCircle = React.forwardRef<
|
||||||
function SkeletonCircle(props, ref) {
|
HTMLDivElement,
|
||||||
|
SkeletonCircleProps
|
||||||
|
>(function SkeletonCircle(props, ref) {
|
||||||
const { size, ...rest } = props;
|
const { size, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Circle size={size} asChild ref={ref}>
|
<Circle size={size} asChild ref={ref}>
|
||||||
<ChakraSkeleton {...rest} />
|
<ChakraSkeleton {...rest} />
|
||||||
</Circle>
|
</Circle>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export interface SkeletonTextProps extends ChakraSkeletonProps {
|
export interface SkeletonTextProps extends ChakraSkeletonProps {
|
||||||
noOfLines?: number;
|
noOfLines?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(function SkeletonText(props, ref) {
|
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
|
||||||
|
function SkeletonText(props, ref) {
|
||||||
const { noOfLines = 3, gap, ...rest } = props;
|
const { noOfLines = 3, gap, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Stack gap={gap} width='full' ref={ref}>
|
<Stack gap={gap} width="full" ref={ref}>
|
||||||
{Array.from({ length: noOfLines }).map((_, index) => (
|
{Array.from({ length: noOfLines }).map((_, index) => (
|
||||||
<ChakraSkeleton height='4' key={index} {...props} _last={{ maxW: '80%' }} {...rest} />
|
<ChakraSkeleton
|
||||||
|
height="4"
|
||||||
|
key={index}
|
||||||
|
{...props}
|
||||||
|
_last={{ maxW: "80%" }}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const Skeleton = ChakraSkeleton;
|
export const Skeleton = ChakraSkeleton;
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import type { ColorPalette } from '@chakra-ui/react';
|
import type { ColorPalette } from "@chakra-ui/react";
|
||||||
import { Status as ChakraStatus } from '@chakra-ui/react';
|
import { Status as ChakraStatus } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
type StatusValue = 'success' | 'error' | 'warning' | 'info';
|
type StatusValue = "success" | "error" | "warning" | "info";
|
||||||
|
|
||||||
export interface StatusProps extends ChakraStatus.RootProps {
|
export interface StatusProps extends ChakraStatus.RootProps {
|
||||||
value?: StatusValue;
|
value?: StatusValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMap: Record<StatusValue, ColorPalette> = {
|
const statusMap: Record<StatusValue, ColorPalette> = {
|
||||||
success: 'green',
|
success: "green",
|
||||||
error: 'red',
|
error: "red",
|
||||||
warning: 'orange',
|
warning: "orange",
|
||||||
info: 'blue',
|
info: "blue",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Status = React.forwardRef<HTMLDivElement, StatusProps>(function Status(props, ref) {
|
export const Status = React.forwardRef<HTMLDivElement, StatusProps>(
|
||||||
const { children, value = 'info', ...rest } = props;
|
function Status(props, ref) {
|
||||||
|
const { children, value = "info", ...rest } = props;
|
||||||
const colorPalette = rest.colorPalette ?? statusMap[value];
|
const colorPalette = rest.colorPalette ?? statusMap[value];
|
||||||
return (
|
return (
|
||||||
<ChakraStatus.Root ref={ref} {...rest} colorPalette={colorPalette}>
|
<ChakraStatus.Root ref={ref} {...rest} colorPalette={colorPalette}>
|
||||||
@@ -24,4 +25,5 @@ export const Status = React.forwardRef<HTMLDivElement, StatusProps>(function Sta
|
|||||||
{children}
|
{children}
|
||||||
</ChakraStatus.Root>
|
</ChakraStatus.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,24 +1,39 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster } from '@chakra-ui/react';
|
import {
|
||||||
|
Toaster as ChakraToaster,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Toast,
|
||||||
|
createToaster,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
export const toaster = createToaster({
|
export const toaster = createToaster({
|
||||||
placement: 'bottom-end',
|
placement: "bottom-end",
|
||||||
pauseOnPageIdle: true,
|
pauseOnPageIdle: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Toaster = () => {
|
export const Toaster = () => {
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
|
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||||
{(toast) => (
|
{(toast) => (
|
||||||
<Toast.Root width={{ md: 'sm' }}>
|
<Toast.Root width={{ md: "sm" }}>
|
||||||
{toast.type === 'loading' ? <Spinner size='sm' color='blue.solid' /> : <Toast.Indicator />}
|
{toast.type === "loading" ? (
|
||||||
<Stack gap='1' flex='1' maxWidth='100%'>
|
<Spinner size="sm" color="blue.solid" />
|
||||||
|
) : (
|
||||||
|
<Toast.Indicator />
|
||||||
|
)}
|
||||||
|
<Stack gap="1" flex="1" maxWidth="100%">
|
||||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||||
{toast.description && <Toast.Description>{toast.description}</Toast.Description>}
|
{toast.description && (
|
||||||
|
<Toast.Description>{toast.description}</Toast.Description>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
|
{toast.action && (
|
||||||
|
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||||
|
)}
|
||||||
{toast.closable && <Toast.CloseTrigger />}
|
{toast.closable && <Toast.CloseTrigger />}
|
||||||
</Toast.Root>
|
</Toast.Root>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CheckboxCard as ChakraCheckboxCard } from '@chakra-ui/react';
|
import { CheckboxCard as ChakraCheckboxCard } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
|
export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
|
||||||
icon?: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
@@ -7,11 +7,14 @@ export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
|
|||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
addon?: React.ReactNode;
|
addon?: React.ReactNode;
|
||||||
indicator?: React.ReactNode | null;
|
indicator?: React.ReactNode | null;
|
||||||
indicatorPlacement?: 'start' | 'end' | 'inside';
|
indicatorPlacement?: "start" | "end" | "inside";
|
||||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CheckboxCard = React.forwardRef<HTMLInputElement, CheckboxCardProps>(function CheckboxCard(props, ref) {
|
export const CheckboxCard = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
CheckboxCardProps
|
||||||
|
>(function CheckboxCard(props, ref) {
|
||||||
const {
|
const {
|
||||||
inputProps,
|
inputProps,
|
||||||
label,
|
label,
|
||||||
@@ -19,27 +22,35 @@ export const CheckboxCard = React.forwardRef<HTMLInputElement, CheckboxCardProps
|
|||||||
icon,
|
icon,
|
||||||
addon,
|
addon,
|
||||||
indicator = <ChakraCheckboxCard.Indicator />,
|
indicator = <ChakraCheckboxCard.Indicator />,
|
||||||
indicatorPlacement = 'end',
|
indicatorPlacement = "end",
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasContent = label || description || icon;
|
const hasContent = label || description || icon;
|
||||||
const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment;
|
const ContentWrapper = indicator
|
||||||
|
? ChakraCheckboxCard.Content
|
||||||
|
: React.Fragment;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChakraCheckboxCard.Root {...rest}>
|
<ChakraCheckboxCard.Root {...rest}>
|
||||||
<ChakraCheckboxCard.HiddenInput ref={ref} {...inputProps} />
|
<ChakraCheckboxCard.HiddenInput ref={ref} {...inputProps} />
|
||||||
<ChakraCheckboxCard.Control>
|
<ChakraCheckboxCard.Control>
|
||||||
{indicatorPlacement === 'start' && indicator}
|
{indicatorPlacement === "start" && indicator}
|
||||||
{hasContent && (
|
{hasContent && (
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
{icon}
|
{icon}
|
||||||
{label && <ChakraCheckboxCard.Label>{label}</ChakraCheckboxCard.Label>}
|
{label && (
|
||||||
{description && <ChakraCheckboxCard.Description>{description}</ChakraCheckboxCard.Description>}
|
<ChakraCheckboxCard.Label>{label}</ChakraCheckboxCard.Label>
|
||||||
{indicatorPlacement === 'inside' && indicator}
|
)}
|
||||||
|
{description && (
|
||||||
|
<ChakraCheckboxCard.Description>
|
||||||
|
{description}
|
||||||
|
</ChakraCheckboxCard.Description>
|
||||||
|
)}
|
||||||
|
{indicatorPlacement === "inside" && indicator}
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
)}
|
)}
|
||||||
{indicatorPlacement === 'end' && indicator}
|
{indicatorPlacement === "end" && indicator}
|
||||||
</ChakraCheckboxCard.Control>
|
</ChakraCheckboxCard.Control>
|
||||||
{addon && <ChakraCheckboxCard.Addon>{addon}</ChakraCheckboxCard.Addon>}
|
{addon && <ChakraCheckboxCard.Addon>{addon}</ChakraCheckboxCard.Addon>}
|
||||||
</ChakraCheckboxCard.Root>
|
</ChakraCheckboxCard.Root>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Checkbox as ChakraCheckbox } from '@chakra-ui/react';
|
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
@@ -7,13 +7,19 @@ export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
|||||||
rootRef?: React.RefObject<HTMLLabelElement | null>;
|
rootRef?: React.RefObject<HTMLLabelElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(props, ref) {
|
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||||
|
function Checkbox(props, ref) {
|
||||||
const { icon, children, inputProps, rootRef, ...rest } = props;
|
const { icon, children, inputProps, rootRef, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||||
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||||
<ChakraCheckbox.Control>{icon || <ChakraCheckbox.Indicator />}</ChakraCheckbox.Control>
|
<ChakraCheckbox.Control>
|
||||||
{children != null && <ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>}
|
{icon || <ChakraCheckbox.Indicator />}
|
||||||
|
</ChakraCheckbox.Control>
|
||||||
|
{children != null && (
|
||||||
|
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||||
|
)}
|
||||||
</ChakraCheckbox.Root>
|
</ChakraCheckbox.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import type { IconButtonProps, StackProps } from '@chakra-ui/react';
|
import type { IconButtonProps, StackProps } from "@chakra-ui/react";
|
||||||
import { ColorPicker as ChakraColorPicker, For, IconButton, Portal, Span, Stack, Text, VStack } from '@chakra-ui/react';
|
import {
|
||||||
import * as React from 'react';
|
ColorPicker as ChakraColorPicker,
|
||||||
import { LuCheck, LuPipette } from 'react-icons/lu';
|
For,
|
||||||
|
IconButton,
|
||||||
|
Portal,
|
||||||
|
Span,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { LuCheck, LuPipette } from "react-icons/lu";
|
||||||
|
|
||||||
export const ColorPickerTrigger = React.forwardRef<
|
export const ColorPickerTrigger = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
@@ -9,7 +18,11 @@ export const ColorPickerTrigger = React.forwardRef<
|
|||||||
>(function ColorPickerTrigger(props, ref) {
|
>(function ColorPickerTrigger(props, ref) {
|
||||||
const { fitContent, ...rest } = props;
|
const { fitContent, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.Trigger data-fit-content={fitContent || undefined} ref={ref} {...rest}>
|
<ChakraColorPicker.Trigger
|
||||||
|
data-fit-content={fitContent || undefined}
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{props.children || <ChakraColorPicker.ValueSwatch />}
|
{props.children || <ChakraColorPicker.ValueSwatch />}
|
||||||
</ChakraColorPicker.Trigger>
|
</ChakraColorPicker.Trigger>
|
||||||
);
|
);
|
||||||
@@ -17,9 +30,9 @@ export const ColorPickerTrigger = React.forwardRef<
|
|||||||
|
|
||||||
export const ColorPickerInput = React.forwardRef<
|
export const ColorPickerInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
Omit<ChakraColorPicker.ChannelInputProps, 'channel'>
|
Omit<ChakraColorPicker.ChannelInputProps, "channel">
|
||||||
>(function ColorHexInput(props, ref) {
|
>(function ColorHexInput(props, ref) {
|
||||||
return <ChakraColorPicker.ChannelInput channel='hex' ref={ref} {...props} />;
|
return <ChakraColorPicker.ChannelInput channel="hex" ref={ref} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ColorPickerContentProps extends ChakraColorPicker.ContentProps {
|
interface ColorPickerContentProps extends ChakraColorPicker.ContentProps {
|
||||||
@@ -27,8 +40,10 @@ interface ColorPickerContentProps extends ChakraColorPicker.ContentProps {
|
|||||||
portalRef?: React.RefObject<HTMLElement | null>;
|
portalRef?: React.RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColorPickerContent = React.forwardRef<HTMLDivElement, ColorPickerContentProps>(
|
export const ColorPickerContent = React.forwardRef<
|
||||||
function ColorPickerContent(props, ref) {
|
HTMLDivElement,
|
||||||
|
ColorPickerContentProps
|
||||||
|
>(function ColorPickerContent(props, ref) {
|
||||||
const { portalled = true, portalRef, ...rest } = props;
|
const { portalled = true, portalRef, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Portal disabled={!portalled} container={portalRef}>
|
<Portal disabled={!portalled} container={portalRef}>
|
||||||
@@ -37,68 +52,85 @@ export const ColorPickerContent = React.forwardRef<HTMLDivElement, ColorPickerCo
|
|||||||
</ChakraColorPicker.Positioner>
|
</ChakraColorPicker.Positioner>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerInlineContent = React.forwardRef<HTMLDivElement, ChakraColorPicker.ContentProps>(
|
export const ColorPickerInlineContent = React.forwardRef<
|
||||||
function ColorPickerInlineContent(props, ref) {
|
HTMLDivElement,
|
||||||
return <ChakraColorPicker.Content animation='none' shadow='none' padding='0' ref={ref} {...props} />;
|
ChakraColorPicker.ContentProps
|
||||||
},
|
>(function ColorPickerInlineContent(props, ref) {
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerSliders = React.forwardRef<HTMLDivElement, StackProps>(function ColorPickerSliders(props, ref) {
|
|
||||||
return (
|
return (
|
||||||
<Stack gap='1' flex='1' px='1' ref={ref} {...props}>
|
<ChakraColorPicker.Content
|
||||||
<ColorPickerChannelSlider channel='hue' />
|
animation="none"
|
||||||
<ColorPickerChannelSlider channel='alpha' />
|
shadow="none"
|
||||||
</Stack>
|
padding="0"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ColorPickerArea = React.forwardRef<HTMLDivElement, ChakraColorPicker.AreaProps>(
|
export const ColorPickerSliders = React.forwardRef<HTMLDivElement, StackProps>(
|
||||||
function ColorPickerArea(props, ref) {
|
function ColorPickerSliders(props, ref) {
|
||||||
|
return (
|
||||||
|
<Stack gap="1" flex="1" px="1" ref={ref} {...props}>
|
||||||
|
<ColorPickerChannelSlider channel="hue" />
|
||||||
|
<ColorPickerChannelSlider channel="alpha" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ColorPickerArea = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
ChakraColorPicker.AreaProps
|
||||||
|
>(function ColorPickerArea(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.Area ref={ref} {...props}>
|
<ChakraColorPicker.Area ref={ref} {...props}>
|
||||||
<ChakraColorPicker.AreaBackground />
|
<ChakraColorPicker.AreaBackground />
|
||||||
<ChakraColorPicker.AreaThumb />
|
<ChakraColorPicker.AreaThumb />
|
||||||
</ChakraColorPicker.Area>
|
</ChakraColorPicker.Area>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerEyeDropper = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
export const ColorPickerEyeDropper = React.forwardRef<
|
||||||
function ColorPickerEyeDropper(props, ref) {
|
HTMLButtonElement,
|
||||||
|
IconButtonProps
|
||||||
|
>(function ColorPickerEyeDropper(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.EyeDropperTrigger asChild>
|
<ChakraColorPicker.EyeDropperTrigger asChild>
|
||||||
<IconButton size='xs' variant='outline' ref={ref} {...props}>
|
<IconButton size="xs" variant="outline" ref={ref} {...props}>
|
||||||
<LuPipette />
|
<LuPipette />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ChakraColorPicker.EyeDropperTrigger>
|
</ChakraColorPicker.EyeDropperTrigger>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerChannelSlider = React.forwardRef<HTMLDivElement, ChakraColorPicker.ChannelSliderProps>(
|
export const ColorPickerChannelSlider = React.forwardRef<
|
||||||
function ColorPickerSlider(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraColorPicker.ChannelSliderProps
|
||||||
|
>(function ColorPickerSlider(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.ChannelSlider ref={ref} {...props}>
|
<ChakraColorPicker.ChannelSlider ref={ref} {...props}>
|
||||||
<ChakraColorPicker.TransparencyGrid size='0.6rem' />
|
<ChakraColorPicker.TransparencyGrid size="0.6rem" />
|
||||||
<ChakraColorPicker.ChannelSliderTrack />
|
<ChakraColorPicker.ChannelSliderTrack />
|
||||||
<ChakraColorPicker.ChannelSliderThumb />
|
<ChakraColorPicker.ChannelSliderThumb />
|
||||||
</ChakraColorPicker.ChannelSlider>
|
</ChakraColorPicker.ChannelSlider>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerSwatchTrigger = React.forwardRef<
|
export const ColorPickerSwatchTrigger = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
ChakraColorPicker.SwatchTriggerProps & {
|
ChakraColorPicker.SwatchTriggerProps & {
|
||||||
swatchSize?: ChakraColorPicker.SwatchTriggerProps['boxSize'];
|
swatchSize?: ChakraColorPicker.SwatchTriggerProps["boxSize"];
|
||||||
}
|
}
|
||||||
>(function ColorPickerSwatchTrigger(props, ref) {
|
>(function ColorPickerSwatchTrigger(props, ref) {
|
||||||
const { swatchSize, children, ...rest } = props;
|
const { swatchSize, children, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.SwatchTrigger ref={ref} style={{ ['--color' as string]: props.value }} {...rest}>
|
<ChakraColorPicker.SwatchTrigger
|
||||||
|
ref={ref}
|
||||||
|
style={{ ["--color" as string]: props.value }}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{children || (
|
{children || (
|
||||||
<ChakraColorPicker.Swatch boxSize={swatchSize} value={props.value}>
|
<ChakraColorPicker.Swatch boxSize={swatchSize} value={props.value}>
|
||||||
<ChakraColorPicker.SwatchIndicator>
|
<ChakraColorPicker.SwatchIndicator>
|
||||||
@@ -110,51 +142,66 @@ export const ColorPickerSwatchTrigger = React.forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ColorPickerRoot = React.forwardRef<HTMLDivElement, ChakraColorPicker.RootProps>(
|
export const ColorPickerRoot = React.forwardRef<
|
||||||
function ColorPickerRoot(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraColorPicker.RootProps
|
||||||
|
>(function ColorPickerRoot(props, ref) {
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.Root ref={ref} {...props}>
|
<ChakraColorPicker.Root ref={ref} {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
<ChakraColorPicker.HiddenInput tabIndex={-1} />
|
<ChakraColorPicker.HiddenInput tabIndex={-1} />
|
||||||
</ChakraColorPicker.Root>
|
</ChakraColorPicker.Root>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const formatMap = {
|
const formatMap = {
|
||||||
rgba: ['red', 'green', 'blue', 'alpha'],
|
rgba: ["red", "green", "blue", "alpha"],
|
||||||
hsla: ['hue', 'saturation', 'lightness', 'alpha'],
|
hsla: ["hue", "saturation", "lightness", "alpha"],
|
||||||
hsba: ['hue', 'saturation', 'brightness', 'alpha'],
|
hsba: ["hue", "saturation", "brightness", "alpha"],
|
||||||
hexa: ['hex', 'alpha'],
|
hexa: ["hex", "alpha"],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ColorPickerChannelInputs = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
|
export const ColorPickerChannelInputs = React.forwardRef<
|
||||||
function ColorPickerChannelInputs(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraColorPicker.ViewProps
|
||||||
|
>(function ColorPickerChannelInputs(props, ref) {
|
||||||
const channels = formatMap[props.format];
|
const channels = formatMap[props.format];
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.View flexDirection='row' ref={ref} {...props}>
|
<ChakraColorPicker.View flexDirection="row" ref={ref} {...props}>
|
||||||
{channels.map((channel) => (
|
{channels.map((channel) => (
|
||||||
<VStack gap='1' key={channel} flex='1'>
|
<VStack gap="1" key={channel} flex="1">
|
||||||
<ColorPickerChannelInput channel={channel} px='0' height='7' textStyle='xs' textAlign='center' />
|
<ColorPickerChannelInput
|
||||||
<Text textStyle='xs' color='fg.muted' fontWeight='medium'>
|
channel={channel}
|
||||||
|
px="0"
|
||||||
|
height="7"
|
||||||
|
textStyle="xs"
|
||||||
|
textAlign="center"
|
||||||
|
/>
|
||||||
|
<Text textStyle="xs" color="fg.muted" fontWeight="medium">
|
||||||
{channel.charAt(0).toUpperCase()}
|
{channel.charAt(0).toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
))}
|
))}
|
||||||
</ChakraColorPicker.View>
|
</ChakraColorPicker.View>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerChannelSliders = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
|
export const ColorPickerChannelSliders = React.forwardRef<
|
||||||
function ColorPickerChannelSliders(props, ref) {
|
HTMLDivElement,
|
||||||
|
ChakraColorPicker.ViewProps
|
||||||
|
>(function ColorPickerChannelSliders(props, ref) {
|
||||||
const channels = formatMap[props.format];
|
const channels = formatMap[props.format];
|
||||||
return (
|
return (
|
||||||
<ChakraColorPicker.View {...props} ref={ref}>
|
<ChakraColorPicker.View {...props} ref={ref}>
|
||||||
<For each={channels}>
|
<For each={channels}>
|
||||||
{(channel) => (
|
{(channel) => (
|
||||||
<Stack gap='1' key={channel}>
|
<Stack gap="1" key={channel}>
|
||||||
<Span textStyle='xs' minW='5ch' textTransform='capitalize' fontWeight='medium'>
|
<Span
|
||||||
|
textStyle="xs"
|
||||||
|
minW="5ch"
|
||||||
|
textTransform="capitalize"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
{channel}
|
{channel}
|
||||||
</Span>
|
</Span>
|
||||||
<ColorPickerChannelSlider channel={channel} />
|
<ColorPickerChannelSlider channel={channel} />
|
||||||
@@ -163,8 +210,7 @@ export const ColorPickerChannelSliders = React.forwardRef<HTMLDivElement, Chakra
|
|||||||
</For>
|
</For>
|
||||||
</ChakraColorPicker.View>
|
</ChakraColorPicker.View>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const ColorPickerLabel = ChakraColorPicker.Label;
|
export const ColorPickerLabel = ChakraColorPicker.Label;
|
||||||
export const ColorPickerControl = ChakraColorPicker.Control;
|
export const ColorPickerControl = ChakraColorPicker.Control;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Field as ChakraField } from '@chakra-ui/react';
|
import { Field as ChakraField } from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
|
|
||||||
export interface FieldProps extends Omit<ChakraField.RootProps, 'label'> {
|
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
helperText?: React.ReactNode;
|
helperText?: React.ReactNode;
|
||||||
errorText?: React.ReactNode;
|
errorText?: React.ReactNode;
|
||||||
optionalText?: React.ReactNode;
|
optionalText?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(function Field(props, ref) {
|
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||||
const { label, children, helperText, errorText, optionalText, ...rest } = props;
|
function Field(props, ref) {
|
||||||
|
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||||
|
props;
|
||||||
return (
|
return (
|
||||||
<ChakraField.Root ref={ref} {...rest}>
|
<ChakraField.Root ref={ref} {...rest}>
|
||||||
{label && (
|
{label && (
|
||||||
@@ -19,8 +21,13 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(function Field
|
|||||||
</ChakraField.Label>
|
</ChakraField.Label>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
{helperText && <ChakraField.HelperText>{helperText}</ChakraField.HelperText>}
|
{helperText && (
|
||||||
{errorText && <ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>}
|
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||||
|
)}
|
||||||
|
{errorText && (
|
||||||
|
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||||
|
)}
|
||||||
</ChakraField.Root>
|
</ChakraField.Root>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import type { ButtonProps, RecipeProps } from '@chakra-ui/react';
|
import type { ButtonProps, RecipeProps } from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FileUpload as ChakraFileUpload,
|
FileUpload as ChakraFileUpload,
|
||||||
@@ -10,16 +10,18 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useFileUploadContext,
|
useFileUploadContext,
|
||||||
useRecipe,
|
useRecipe,
|
||||||
} from '@chakra-ui/react';
|
} from "@chakra-ui/react";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { LuFile, LuUpload, LuX } from 'react-icons/lu';
|
import { LuFile, LuUpload, LuX } from "react-icons/lu";
|
||||||
|
|
||||||
export interface FileUploadRootProps extends ChakraFileUpload.RootProps {
|
export interface FileUploadRootProps extends ChakraFileUpload.RootProps {
|
||||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadRoot = React.forwardRef<HTMLInputElement, FileUploadRootProps>(
|
export const FileUploadRoot = React.forwardRef<
|
||||||
function FileUploadRoot(props, ref) {
|
HTMLInputElement,
|
||||||
|
FileUploadRootProps
|
||||||
|
>(function FileUploadRoot(props, ref) {
|
||||||
const { children, inputProps, ...rest } = props;
|
const { children, inputProps, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraFileUpload.Root {...rest}>
|
<ChakraFileUpload.Root {...rest}>
|
||||||
@@ -27,31 +29,32 @@ export const FileUploadRoot = React.forwardRef<HTMLInputElement, FileUploadRootP
|
|||||||
{children}
|
{children}
|
||||||
</ChakraFileUpload.Root>
|
</ChakraFileUpload.Root>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export interface FileUploadDropzoneProps extends ChakraFileUpload.DropzoneProps {
|
export interface FileUploadDropzoneProps
|
||||||
|
extends ChakraFileUpload.DropzoneProps {
|
||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadDropzone = React.forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
|
export const FileUploadDropzone = React.forwardRef<
|
||||||
function FileUploadDropzone(props, ref) {
|
HTMLInputElement,
|
||||||
|
FileUploadDropzoneProps
|
||||||
|
>(function FileUploadDropzone(props, ref) {
|
||||||
const { children, label, description, ...rest } = props;
|
const { children, label, description, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraFileUpload.Dropzone ref={ref} {...rest}>
|
<ChakraFileUpload.Dropzone ref={ref} {...rest}>
|
||||||
<Icon fontSize='xl' color='fg.muted'>
|
<Icon fontSize="xl" color="fg.muted">
|
||||||
<LuUpload />
|
<LuUpload />
|
||||||
</Icon>
|
</Icon>
|
||||||
<ChakraFileUpload.DropzoneContent>
|
<ChakraFileUpload.DropzoneContent>
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
{description && <Text color='fg.muted'>{description}</Text>}
|
{description && <Text color="fg.muted">{description}</Text>}
|
||||||
</ChakraFileUpload.DropzoneContent>
|
</ChakraFileUpload.DropzoneContent>
|
||||||
{children}
|
{children}
|
||||||
</ChakraFileUpload.Dropzone>
|
</ChakraFileUpload.Dropzone>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface VisibilityProps {
|
interface VisibilityProps {
|
||||||
showSize?: boolean;
|
showSize?: boolean;
|
||||||
@@ -62,12 +65,13 @@ interface FileUploadItemProps extends VisibilityProps {
|
|||||||
file: File;
|
file: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(function FileUploadItem(props, ref) {
|
const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(
|
||||||
|
function FileUploadItem(props, ref) {
|
||||||
const { file, showSize, clearable } = props;
|
const { file, showSize, clearable } = props;
|
||||||
return (
|
return (
|
||||||
<ChakraFileUpload.Item file={file} ref={ref}>
|
<ChakraFileUpload.Item file={file} ref={ref}>
|
||||||
<ChakraFileUpload.ItemPreview asChild>
|
<ChakraFileUpload.ItemPreview asChild>
|
||||||
<Icon fontSize='lg' color='fg.muted'>
|
<Icon fontSize="lg" color="fg.muted">
|
||||||
<LuFile />
|
<LuFile />
|
||||||
</Icon>
|
</Icon>
|
||||||
</ChakraFileUpload.ItemPreview>
|
</ChakraFileUpload.ItemPreview>
|
||||||
@@ -78,26 +82,30 @@ const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(func
|
|||||||
<ChakraFileUpload.ItemSizeText />
|
<ChakraFileUpload.ItemSizeText />
|
||||||
</ChakraFileUpload.ItemContent>
|
</ChakraFileUpload.ItemContent>
|
||||||
) : (
|
) : (
|
||||||
<ChakraFileUpload.ItemName flex='1' />
|
<ChakraFileUpload.ItemName flex="1" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{clearable && (
|
{clearable && (
|
||||||
<ChakraFileUpload.ItemDeleteTrigger asChild>
|
<ChakraFileUpload.ItemDeleteTrigger asChild>
|
||||||
<IconButton variant='ghost' color='fg.muted' size='xs'>
|
<IconButton variant="ghost" color="fg.muted" size="xs">
|
||||||
<LuX />
|
<LuX />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ChakraFileUpload.ItemDeleteTrigger>
|
</ChakraFileUpload.ItemDeleteTrigger>
|
||||||
)}
|
)}
|
||||||
</ChakraFileUpload.Item>
|
</ChakraFileUpload.Item>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
interface FileUploadListProps extends VisibilityProps, ChakraFileUpload.ItemGroupProps {
|
interface FileUploadListProps
|
||||||
|
extends VisibilityProps, ChakraFileUpload.ItemGroupProps {
|
||||||
files?: File[];
|
files?: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadList = React.forwardRef<HTMLUListElement, FileUploadListProps>(
|
export const FileUploadList = React.forwardRef<
|
||||||
function FileUploadList(props, ref) {
|
HTMLUListElement,
|
||||||
|
FileUploadListProps
|
||||||
|
>(function FileUploadList(props, ref) {
|
||||||
const { showSize, clearable, files, ...rest } = props;
|
const { showSize, clearable, files, ...rest } = props;
|
||||||
|
|
||||||
const fileUpload = useFileUploadContext();
|
const fileUpload = useFileUploadContext();
|
||||||
@@ -108,26 +116,37 @@ export const FileUploadList = React.forwardRef<HTMLUListElement, FileUploadListP
|
|||||||
return (
|
return (
|
||||||
<ChakraFileUpload.ItemGroup ref={ref} {...rest}>
|
<ChakraFileUpload.ItemGroup ref={ref} {...rest}>
|
||||||
{acceptedFiles.map((file) => (
|
{acceptedFiles.map((file) => (
|
||||||
<FileUploadItem key={file.name} file={file} showSize={showSize} clearable={clearable} />
|
<FileUploadItem
|
||||||
|
key={file.name}
|
||||||
|
file={file}
|
||||||
|
showSize={showSize}
|
||||||
|
clearable={clearable}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ChakraFileUpload.ItemGroup>
|
</ChakraFileUpload.ItemGroup>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
type Assign<T, U> = Omit<T, keyof U> & U;
|
type Assign<T, U> = Omit<T, keyof U> & U;
|
||||||
|
|
||||||
interface FileInputProps extends Assign<ButtonProps, RecipeProps<'input'>> {
|
interface FileInputProps extends Assign<ButtonProps, RecipeProps<"input">> {
|
||||||
placeholder?: React.ReactNode;
|
placeholder?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(function FileInput(props, ref) {
|
export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(
|
||||||
const inputRecipe = useRecipe({ key: 'input' });
|
function FileInput(props, ref) {
|
||||||
|
const inputRecipe = useRecipe({ key: "input" });
|
||||||
const [recipeProps, restProps] = inputRecipe.splitVariantProps(props);
|
const [recipeProps, restProps] = inputRecipe.splitVariantProps(props);
|
||||||
const { placeholder = 'Select file(s)', ...rest } = restProps;
|
const { placeholder = "Select file(s)", ...rest } = restProps;
|
||||||
return (
|
return (
|
||||||
<ChakraFileUpload.Trigger asChild>
|
<ChakraFileUpload.Trigger asChild>
|
||||||
<Button unstyled py='0' ref={ref} {...rest} css={[inputRecipe(recipeProps), props.css]}>
|
<Button
|
||||||
|
unstyled
|
||||||
|
py="0"
|
||||||
|
ref={ref}
|
||||||
|
{...rest}
|
||||||
|
css={[inputRecipe(recipeProps), props.css]}
|
||||||
|
>
|
||||||
<ChakraFileUpload.Context>
|
<ChakraFileUpload.Context>
|
||||||
{({ acceptedFiles }) => {
|
{({ acceptedFiles }) => {
|
||||||
if (acceptedFiles.length === 1) {
|
if (acceptedFiles.length === 1) {
|
||||||
@@ -136,13 +155,14 @@ export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(fun
|
|||||||
if (acceptedFiles.length > 1) {
|
if (acceptedFiles.length > 1) {
|
||||||
return <span>{acceptedFiles.length} files</span>;
|
return <span>{acceptedFiles.length} files</span>;
|
||||||
}
|
}
|
||||||
return <Span color='fg.subtle'>{placeholder}</Span>;
|
return <Span color="fg.subtle">{placeholder}</Span>;
|
||||||
}}
|
}}
|
||||||
</ChakraFileUpload.Context>
|
</ChakraFileUpload.Context>
|
||||||
</Button>
|
</Button>
|
||||||
</ChakraFileUpload.Trigger>
|
</ChakraFileUpload.Trigger>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const FileUploadLabel = ChakraFileUpload.Label;
|
export const FileUploadLabel = ChakraFileUpload.Label;
|
||||||
export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger;
|
export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user