first commit

This commit is contained in:
mstfyldz
2026-03-24 15:46:27 +03:00
parent 095d830279
commit 34b6a46604
33 changed files with 5212 additions and 81 deletions

View File

@@ -0,0 +1,480 @@
import { getAppById } from "../../actions";
import {
getAppDetails,
getAppStoreVersions,
getLatestBuilds,
getBetaGroups,
getCustomerReviews,
} from "../../../asc/actions";
import {
ArrowLeft,
Star,
Package,
Users,
TestTube2,
Globe,
ShoppingBag,
AlertCircle,
CheckCircle2,
Clock,
XCircle,
ChevronRight,
ExternalLink,
Apple,
} from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import LocalizationEditor from "./LocalizationEditor";
import ReviewCard from "./ReviewCard";
import ScreenshotManager from "./ScreenshotManager";
// ─── Helpers ────────────────────────────────────────────────────────────────
function stateColor(state: string) {
const map: Record<string, string> = {
READY_FOR_SALE: "text-emerald-400 bg-emerald-400/10 border-emerald-400/20",
IN_REVIEW: "text-yellow-400 bg-yellow-400/10 border-yellow-400/20",
WAITING_FOR_REVIEW: "text-blue-400 bg-blue-400/10 border-blue-400/20",
PENDING_DEVELOPER_RELEASE: "text-violet-400 bg-violet-400/10 border-violet-400/20",
PREPARE_FOR_SUBMISSION: "text-slate-400 bg-slate-400/10 border-slate-400/20",
REJECTED: "text-red-400 bg-red-400/10 border-red-400/20",
DEVELOPER_REJECTED: "text-orange-400 bg-orange-400/10 border-orange-400/20",
METADATA_REJECTED: "text-orange-400 bg-orange-400/10 border-orange-400/20",
};
return map[state] ?? "text-slate-400 bg-slate-400/10 border-slate-400/20";
}
function stateIcon(state: string) {
if (state === "READY_FOR_SALE") return <CheckCircle2 size={12} />;
if (state === "REJECTED" || state === "DEVELOPER_REJECTED") return <XCircle size={12} />;
if (state?.includes("REVIEW")) return <Clock size={12} />;
return <AlertCircle size={12} />;
}
function stateLabel(state: string) {
const map: Record<string, string> = {
READY_FOR_SALE: "Satışta",
IN_REVIEW: "İncelemede",
WAITING_FOR_REVIEW: "İnceleme Bekliyor",
PENDING_DEVELOPER_RELEASE: "Geliştirici Yayını Bekliyor",
PREPARE_FOR_SUBMISSION: "Gönderime Hazırlanıyor",
REJECTED: "Reddedildi",
DEVELOPER_REJECTED: "Geliştirici Reddetti",
METADATA_REJECTED: "Metadata Reddedildi",
};
return map[state] ?? state;
}
function processingBadge(state: string) {
if (state === "VALID") return <span className="text-emerald-400 text-[10px] font-bold"> Geçerli</span>;
if (state === "PROCESSING") return <span className="text-yellow-400 text-[10px] font-bold animate-pulse"> İşleniyor</span>;
if (state === "FAILED") return <span className="text-red-400 text-[10px] font-bold"> Başarısız</span>;
return <span className="text-slate-400 text-[10px]">{state}</span>;
}
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map((s) => (
<Star
key={s}
size={12}
className={s <= rating ? "fill-yellow-400 text-yellow-400" : "text-slate-700"}
/>
))}
</div>
);
}
// ─── Page ────────────────────────────────────────────────────────────────────
export default async function AppStorePage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ versionId?: string }>;
}) {
const { id } = await params;
const { versionId } = await searchParams;
const appId = parseInt(id);
if (isNaN(appId)) notFound();
const app = await getAppById(appId);
if (!app) notFound();
const hasAppleId = !!app.appleId;
// Paralel fetch
const [detailsRes, versionsRes, buildsRes, betaRes, reviewsRes] =
await Promise.all([
hasAppleId ? getAppDetails(app.appleId!) : Promise.resolve({ data: null, error: null }),
hasAppleId ? getAppStoreVersions(app.appleId!) : Promise.resolve({ data: null, error: null }),
hasAppleId ? getLatestBuilds(app.appleId!) : Promise.resolve({ data: null, error: null }),
hasAppleId ? getBetaGroups(app.appleId!) : Promise.resolve({ data: null, error: null }),
hasAppleId ? getCustomerReviews(app.appleId!) : Promise.resolve({ data: null, error: null }),
]);
const versions: any[] = versionsRes.data?.data ?? [];
const liveVersion = versions[0];
// Use the versionId from the query string if provided, otherwise fall back to the first (live) version
const selectedVersion = versionId
? (versions.find((v: any) => v.id === versionId) ?? liveVersion)
: liveVersion;
const builds: any[] = buildsRes.data?.data ?? [];
const betaGroups: any[] = betaRes.data?.data ?? [];
const reviews: any[] = reviewsRes.data?.data ?? [];
const appInfo = detailsRes.data?.data;
// Avg rating
const avgRating =
reviews.length > 0
? reviews.reduce((s: number, r: any) => s + (r.attributes?.rating ?? 0), 0) / reviews.length
: null;
return (
<div className="min-h-screen bg-[#0a0a0f] text-white">
{/* Header */}
<div className="border-b border-white/5 bg-[#0a0a0f]/80 backdrop-blur sticky top-0 z-20">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center gap-4">
<Link
href={`/apps`}
className="p-2 rounded-lg hover:bg-white/5 transition-colors text-slate-400 hover:text-white"
>
<ArrowLeft size={18} />
</Link>
<div className="flex items-center gap-3 flex-1">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg">
<Apple size={18} className="text-white" />
</div>
<div>
<h1 className="font-bold text-base leading-none">{app.name}</h1>
<p className="text-[11px] text-slate-400 mt-0.5">{app.bundleId}</p>
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1">
{[
{ href: `/apps/${appId}/config`, label: "Remote Config" },
{ href: `/apps/${appId}/store`, label: "App Store", active: true },
].map((tab) => (
<Link
key={tab.href}
href={tab.href}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
tab.active
? "bg-violet-600 text-white shadow"
: "text-slate-400 hover:text-white"
}`}
>
{tab.label}
</Link>
))}
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-6 py-8 space-y-8">
{/* No Apple ID warning */}
{!hasAppleId && (
<div className="rounded-2xl border border-orange-500/20 bg-orange-500/5 p-6 flex items-start gap-4">
<AlertCircle size={20} className="text-orange-400 shrink-0 mt-0.5" />
<div>
<p className="font-semibold text-orange-300">Apple ID Tanımlı Değil</p>
<p className="text-sm text-slate-400 mt-1">
App Store Connect verilerini çekebilmek için bu uygulamaya bir Apple ID ekleyin.
</p>
<Link
href={`/apps/${appId}/edit`}
className="mt-3 inline-flex items-center gap-2 text-xs font-bold text-orange-400 hover:text-orange-300 transition-colors"
>
Düzenle <ChevronRight size={12} />
</Link>
</div>
</div>
)}
{/* ── KPIs ─────────────────────────────────────────────────────── */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{/* Live Version */}
<div className="rounded-2xl border border-white/5 bg-white/[0.03] p-5">
<div className="flex items-center gap-2 text-slate-400 text-xs font-semibold mb-3">
<Package size={13} /> Canlı Sürüm
</div>
{liveVersion ? (
<>
<p className="text-2xl font-bold">
{liveVersion.attributes?.versionString ?? "—"}
</p>
<span
className={`mt-2 inline-flex items-center gap-1 text-[10px] font-bold px-2 py-0.5 rounded-full border ${stateColor(
liveVersion.attributes?.appStoreState
)}`}
>
{stateIcon(liveVersion.attributes?.appStoreState)}
{stateLabel(liveVersion.attributes?.appStoreState)}
</span>
</>
) : (
<p className="text-slate-500 text-sm"></p>
)}
</div>
{/* Avg Rating */}
<div className="rounded-2xl border border-white/5 bg-white/[0.03] p-5">
<div className="flex items-center gap-2 text-slate-400 text-xs font-semibold mb-3">
<Star size={13} /> Ortalama Puan
</div>
{avgRating !== null ? (
<>
<p className="text-2xl font-bold">{avgRating.toFixed(1)}</p>
<StarRating rating={Math.round(avgRating)} />
<p className="text-[10px] text-slate-500 mt-1">{reviews.length} yorum</p>
</>
) : (
<p className="text-slate-500 text-sm"></p>
)}
</div>
{/* Builds */}
<div className="rounded-2xl border border-white/5 bg-white/[0.03] p-5">
<div className="flex items-center gap-2 text-slate-400 text-xs font-semibold mb-3">
<Package size={13} /> Son Build
</div>
{builds[0] ? (
<>
<p className="text-2xl font-bold">
{builds[0].attributes?.version ?? "—"}
</p>
<div className="mt-1">
{processingBadge(builds[0].attributes?.processingState)}
</div>
<p className="text-[10px] text-slate-500 mt-1">
{builds[0].attributes?.uploadedDate
? new Date(builds[0].attributes.uploadedDate).toLocaleDateString("tr-TR")
: ""}
</p>
</>
) : (
<p className="text-slate-500 text-sm"></p>
)}
</div>
{/* Beta Testers */}
<div className="rounded-2xl border border-white/5 bg-white/[0.03] p-5">
<div className="flex items-center gap-2 text-slate-400 text-xs font-semibold mb-3">
<TestTube2 size={13} /> TestFlight Grup
</div>
<p className="text-2xl font-bold">{betaGroups.length}</p>
<p className="text-[10px] text-slate-500 mt-1">aktif grup</p>
</div>
</div>
{/* ── Main Grid ────────────────────────────────────────────────── */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* LEFT: Versions + Localizations */}
<div className="xl:col-span-2 space-y-6">
{/* Sürümler */}
<section className="rounded-2xl border border-white/5 bg-white/[0.03] overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/5">
<h2 className="font-bold text-sm flex items-center gap-2">
<Globe size={15} className="text-violet-400" /> App Store Sürümleri
</h2>
</div>
{versions.length === 0 ? (
<div className="p-6 text-center text-slate-500 text-sm">Sürüm bulunamadı.</div>
) : (
<div className="divide-y divide-white/5">
{versions.map((v: any) => {
const isSelected = selectedVersion?.id === v.id;
return (
<div
key={v.id}
className={`px-6 py-4 flex items-center justify-between transition-colors ${
isSelected
? "bg-violet-500/10 border-l-2 border-violet-500"
: "hover:bg-white/[0.02]"
}`}
>
<div>
<p className="font-semibold text-sm">
v{v.attributes?.versionString}
{isSelected && (
<span className="ml-2 text-[9px] font-bold text-violet-400 uppercase tracking-wider">
Seçili
</span>
)}
</p>
<p className="text-[10px] text-slate-500 mt-0.5">
{v.attributes?.createdDate
? new Date(v.attributes.createdDate).toLocaleDateString("tr-TR", {
year: "numeric", month: "long", day: "numeric",
})
: ""}
{v.attributes?.copyright && ` · ${v.attributes.copyright}`}
</p>
</div>
<div className="flex items-center gap-3">
<span className={`inline-flex items-center gap-1 text-[10px] font-bold px-2.5 py-1 rounded-full border ${stateColor(v.attributes?.appStoreState)}`}>
{stateIcon(v.attributes?.appStoreState)}
{stateLabel(v.attributes?.appStoreState)}
</span>
{/* Localization Link */}
<Link
href={`/apps/${appId}/store?versionId=${v.id}`}
className={`text-[10px] font-semibold flex items-center gap-1 ${
isSelected
? "text-violet-300 cursor-default pointer-events-none"
: "text-violet-400 hover:text-violet-300"
}`}
>
Metadata <ChevronRight size={10} />
</Link>
</div>
</div>
);
})}
</div>
)}
</section>
{/* Localization Editor — shows selected version (or live version) */}
{selectedVersion && (
<LocalizationEditor
versionId={selectedVersion.id}
versionString={selectedVersion.attributes?.versionString}
/>
)}
{/* Screenshot Manager (iPhone 6.5") */}
{selectedVersion && (
<ScreenshotManager
versionId={selectedVersion.id}
versionString={selectedVersion.attributes?.versionString}
/>
)}
{/* Builds */}
<section className="rounded-2xl border border-white/5 bg-white/[0.03] overflow-hidden">
<div className="px-6 py-4 border-b border-white/5">
<h2 className="font-bold text-sm flex items-center gap-2">
<Package size={15} className="text-indigo-400" /> Son Buildler
</h2>
</div>
{builds.length === 0 ? (
<div className="p-6 text-center text-slate-500 text-sm">Build bulunamadı.</div>
) : (
<div className="divide-y divide-white/5">
{builds.map((b: any) => (
<div key={b.id} className="px-6 py-4 flex items-center justify-between hover:bg-white/[0.02]">
<div>
<p className="font-semibold text-sm">Build {b.attributes?.version}</p>
<p className="text-[10px] text-slate-500 mt-0.5">
iOS {b.attributes?.minOsVersion}+
{b.attributes?.uploadedDate
? ` · ${new Date(b.attributes.uploadedDate).toLocaleDateString("tr-TR")}`
: ""}
</p>
</div>
<div>{processingBadge(b.attributes?.processingState)}</div>
</div>
))}
</div>
)}
</section>
</div>
{/* RIGHT: Reviews + TestFlight */}
<div className="space-y-6">
{/* Yorumlar */}
<section className="rounded-2xl border border-white/5 bg-white/[0.03] overflow-hidden">
<div className="flex items-center justify-between px-6 py-4 border-b border-white/5">
<h2 className="font-bold text-sm flex items-center gap-2">
<Star size={15} className="text-yellow-400" /> Kullanıcı Yorumları
</h2>
{avgRating !== null && (
<span className="text-xs text-slate-400 font-medium">{avgRating.toFixed(1)} </span>
)}
</div>
{reviews.length === 0 ? (
<div className="p-6 text-center text-slate-500 text-sm">
{hasAppleId ? "Yorum bulunamadı." : "Apple ID gerekli."}
</div>
) : (
<div className="divide-y divide-white/5 max-h-[480px] overflow-y-auto">
{reviews.map((r: any) => (
<ReviewCard key={r.id} review={r} />
))}
</div>
)}
</section>
{/* TestFlight Grupları */}
<section className="rounded-2xl border border-white/5 bg-white/[0.03] overflow-hidden">
<div className="px-6 py-4 border-b border-white/5">
<h2 className="font-bold text-sm flex items-center gap-2">
<TestTube2 size={15} className="text-sky-400" /> TestFlight Grupları
</h2>
</div>
{betaGroups.length === 0 ? (
<div className="p-6 text-center text-slate-500 text-sm">
{hasAppleId ? "Grup bulunamadı." : "Apple ID gerekli."}
</div>
) : (
<div className="divide-y divide-white/5">
{betaGroups.map((g: any) => (
<div key={g.id} className="px-6 py-4 hover:bg-white/[0.02]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold">{g.attributes?.name}</p>
<p className="text-[10px] text-slate-500 mt-0.5">
{g.attributes?.isInternalGroup ? "İç Grup" : "Dış Grup"}
</p>
</div>
{g.attributes?.publicLinkEnabled && (
<a
href={g.attributes.publicLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-[10px] text-sky-400 hover:text-sky-300 font-semibold"
>
Link <ExternalLink size={9} />
</a>
)}
</div>
{g.attributes?.publicLinkLimitEnabled && (
<p className="text-[10px] text-slate-500 mt-1">
Limit: {g.attributes.publicLinkLimit} kişi
</p>
)}
</div>
))}
</div>
)}
</section>
{/* App Store Link */}
{appInfo && (
<a
href={`https://apps.apple.com/app/id${app.appleId}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between w-full px-5 py-4 rounded-2xl border border-violet-500/20 bg-violet-500/5 hover:bg-violet-500/10 transition-colors group"
>
<div className="flex items-center gap-3">
<ShoppingBag size={16} className="text-violet-400" />
<span className="text-sm font-semibold text-violet-300">App Store'da Görüntüle</span>
</div>
<ExternalLink size={14} className="text-violet-400 group-hover:translate-x-0.5 transition-transform" />
</a>
)}
</div>
</div>
</div>
</div>
);
}