first commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AppIconProps {
|
||||
name: string
|
||||
iconUrl?: string | null
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
}
|
||||
|
||||
export function AppIcon({ name, iconUrl, className, size = 'md' }: AppIconProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-7 h-7 text-xs rounded-lg',
|
||||
md: 'w-10 h-10 text-base rounded-xl',
|
||||
lg: 'w-12 h-12 text-lg rounded-2xl',
|
||||
xl: 'w-16 h-16 text-2xl rounded-3xl',
|
||||
}[size]
|
||||
|
||||
// If iconUrl is an image (starts with http, / or data:image)
|
||||
const isImage = iconUrl && (
|
||||
iconUrl.startsWith('http://') ||
|
||||
iconUrl.startsWith('https://') ||
|
||||
iconUrl.startsWith('/') ||
|
||||
iconUrl.startsWith('data:image/')
|
||||
)
|
||||
|
||||
// Gradient generator from app name
|
||||
const initial = name ? name.charAt(0).toUpperCase() : 'A'
|
||||
const gradients = [
|
||||
'from-emerald-500 to-teal-700',
|
||||
'from-cyan-500 to-blue-700',
|
||||
'from-indigo-500 to-purple-700',
|
||||
'from-amber-500 to-orange-700',
|
||||
'from-rose-500 to-pink-700',
|
||||
'from-violet-500 to-fuchsia-700',
|
||||
]
|
||||
const gradientIndex = (name.charCodeAt(0) || 0) % gradients.length
|
||||
const bgGradient = gradients[gradientIndex]
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<div className={cn('relative overflow-hidden bg-slate-900 border border-slate-800 shadow-md flex items-center justify-center flex-shrink-0', sizeClasses, className)}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={iconUrl}
|
||||
alt={name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// fallback if image fails to load
|
||||
e.currentTarget.style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If iconUrl is an emoji or custom text
|
||||
if (iconUrl && iconUrl.trim()) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'bg-slate-900 border border-slate-800/80 shadow-md flex items-center justify-center flex-shrink-0 select-none',
|
||||
sizeClasses,
|
||||
className
|
||||
)}>
|
||||
<span className="leading-none">{iconUrl}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback initial with background gradient
|
||||
return (
|
||||
<div className={cn(
|
||||
`bg-gradient-to-br ${bgGradient} p-[1px] shadow-lg flex items-center justify-center text-white font-extrabold flex-shrink-0 select-none`,
|
||||
sizeClasses,
|
||||
className
|
||||
)}>
|
||||
<span className="drop-shadow-sm">{initial}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Plus, Search, Layers, Key, ArrowUpRight, Clock, Sparkles, SlidersHorizontal, ShieldCheck, Zap } from 'lucide-react'
|
||||
import { App } from '@/types'
|
||||
import { AppIcon } from './AppIcon'
|
||||
|
||||
interface DashboardApp extends App {
|
||||
config_count?: number | string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
initialApps: DashboardApp[]
|
||||
totalConfigsCount: number
|
||||
totalAuditCount: number
|
||||
}
|
||||
|
||||
export function DashboardClient({ initialApps, totalConfigsCount, totalAuditCount }: Props) {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filteredApps = initialApps.filter(app =>
|
||||
app.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.slug.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(app.description && app.description.toLowerCase().includes(search.toLowerCase()))
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
{/* Hero / Stat cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Stat 1 */}
|
||||
<div className="glass-panel p-5 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-emerald-500/10 rounded-full blur-2xl group-hover:bg-emerald-500/20 transition-colors" />
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Kayıtlı Uygulamalar</span>
|
||||
<div className="w-8 h-8 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400">
|
||||
<Layers className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold text-white tracking-tight">{initialApps.length}</span>
|
||||
<span className="text-xs text-emerald-400 font-medium">Aktif Servis</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat 2 */}
|
||||
<div className="glass-panel p-5 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-cyan-500/10 rounded-full blur-2xl group-hover:bg-cyan-500/20 transition-colors" />
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Toplam Config & Secret</span>
|
||||
<div className="w-8 h-8 rounded-xl bg-cyan-500/10 border border-cyan-500/20 flex items-center justify-center text-cyan-400">
|
||||
<Key className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold text-white tracking-tight">{totalConfigsCount}</span>
|
||||
<span className="text-xs text-cyan-400 font-medium">Değer Tanımlı</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat 3 */}
|
||||
<div className="glass-panel p-5 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-purple-500/10 rounded-full blur-2xl group-hover:bg-purple-500/20 transition-colors" />
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Ortam Desteği</span>
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400">
|
||||
<Zap className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold text-white tracking-tight">3</span>
|
||||
<span className="text-xs text-purple-400 font-medium">Dev · Stage · Prod</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat 4 */}
|
||||
<div className="glass-panel p-5 rounded-2xl relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-amber-500/10 rounded-full blur-2xl group-hover:bg-amber-500/20 transition-colors" />
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">İşlem Denetimi</span>
|
||||
<div className="w-8 h-8 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-extrabold text-white tracking-tight">{totalAuditCount}</span>
|
||||
<span className="text-xs text-amber-400 font-medium">Audit Kaydı</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Header & Search */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-2">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Uygulama adı veya slug ara..."
|
||||
className="w-full glass-input rounded-xl pl-10 pr-4 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-400 hover:text-white"
|
||||
>
|
||||
Temizle
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/dashboard/apps/new"
|
||||
className="inline-flex items-center justify-center gap-2 bg-emerald-500 hover:bg-emerald-400 active:bg-emerald-600 text-slate-950 font-bold text-sm px-5 py-2.5 rounded-xl shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.02]"
|
||||
>
|
||||
<Plus className="w-4 h-4 stroke-[2.5]" />
|
||||
<span>Yeni Uygulama Ekle</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* App Cards Grid */}
|
||||
{filteredApps.length === 0 ? (
|
||||
<div className="glass-panel text-center py-16 px-4 rounded-3xl border border-dashed border-slate-800">
|
||||
<div className="w-14 h-14 bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center mx-auto mb-4 text-slate-500">
|
||||
<SlidersHorizontal className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="text-white text-lg font-bold mb-1">
|
||||
{search ? 'Eşleşen uygulama bulunamadı' : 'Henüz hiç uygulama eklenmemiş'}
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm max-w-md mx-auto mb-6">
|
||||
{search
|
||||
? `"${search}" aramasıyla eşleşen bir sonuç yok. Başka bir anahtar kelime deneyin.`
|
||||
: 'ConfigVault ile mobil ve web uygulamalarınızın ortam değişkenlerini dinamik olarak yönetmeye başlamak için ilk uygulamanızı ekleyin.'}
|
||||
</p>
|
||||
{!search && (
|
||||
<Link
|
||||
href="/dashboard/apps/new"
|
||||
className="inline-flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold text-sm px-5 py-2.5 rounded-xl shadow-lg shadow-emerald-500/25 transition-transform hover:scale-105"
|
||||
>
|
||||
<Plus className="w-4 h-4 stroke-[2.5]" />
|
||||
<span>İlk Uygulamayı Oluştur</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{filteredApps.map(app => (
|
||||
<AppCard key={app.id} app={app} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AppCard({ app }: { app: DashboardApp }) {
|
||||
const formattedDate = new Date(app.updated_at).toLocaleDateString('tr-TR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
// Color theme generator based on app name initial
|
||||
const initial = app.name.charAt(0).toUpperCase()
|
||||
const gradients = [
|
||||
'from-emerald-500 to-teal-700',
|
||||
'from-cyan-500 to-blue-700',
|
||||
'from-indigo-500 to-purple-700',
|
||||
'from-amber-500 to-orange-700',
|
||||
'from-rose-500 to-pink-700',
|
||||
]
|
||||
const gradientIndex = (initial.charCodeAt(0) || 0) % gradients.length
|
||||
const bgGradient = gradients[gradientIndex]
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/apps/${app.id}`}
|
||||
className="glass-panel-interactive rounded-2xl p-6 flex flex-col justify-between group relative overflow-hidden"
|
||||
>
|
||||
{/* Ambient background hover flare */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/5 group-hover:bg-emerald-500/10 rounded-full blur-2xl transition-all duration-300 pointer-events-none" />
|
||||
|
||||
<div>
|
||||
{/* Top bar: Avatar & Arrow */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<AppIcon name={app.name} iconUrl={app.icon_url} size="lg" />
|
||||
<div className="w-8 h-8 rounded-xl bg-slate-800/60 border border-slate-700/50 flex items-center justify-center text-slate-400 group-hover:text-emerald-400 group-hover:border-emerald-500/30 group-hover:bg-emerald-500/10 transition-all">
|
||||
<ArrowUpRight className="w-4 h-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title & Slug */}
|
||||
<h3 className="text-white text-lg font-bold group-hover:text-emerald-300 transition-colors tracking-tight">
|
||||
{app.name}
|
||||
</h3>
|
||||
<div className="inline-flex items-center gap-1 mt-1 px-2 py-0.5 rounded-md bg-slate-900/90 border border-slate-800 text-[11px] font-mono text-slate-400">
|
||||
<span>slug:</span>
|
||||
<span className="text-emerald-400 font-semibold">{app.slug}</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-slate-400 text-xs mt-3 line-clamp-2 leading-relaxed min-h-[32px]">
|
||||
{app.description || <span className="text-slate-600 italic">Açıklama girilmemiş</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom meta details */}
|
||||
<div className="mt-6 pt-4 border-t border-slate-800/80 space-y-3">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-1.5 text-slate-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
<span className="font-mono text-[11px]">
|
||||
{Number(app.config_count) || 0} config kaydı
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-slate-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{formattedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Env tag pills */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||||
prod
|
||||
</span>
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20">
|
||||
stage
|
||||
</span>
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
dev
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef } from 'react'
|
||||
import { Upload, Link as LinkIcon, Sparkles, X, Image as ImageIcon } from 'lucide-react'
|
||||
import { AppIcon } from './AppIcon'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface IconPickerProps {
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
appName: string
|
||||
}
|
||||
|
||||
const PRESET_EMOJIS = [
|
||||
'📱', '🚀', '🛍️', '💳', '⚡', '🛡️', '🤖', '📦',
|
||||
'🎮', '📊', '🎧', '💬', '🌐', '🔒', '🍔', '🚗',
|
||||
'✈️', '📈', '🎨', '🎵', '🏥', '🏢', '⚙️', '🔑'
|
||||
]
|
||||
|
||||
export function IconPicker({ value, onChange, appName }: IconPickerProps) {
|
||||
const [activeTab, setActiveTab] = useState<'presets' | 'url' | 'upload'>('presets')
|
||||
const [urlInput, setUrlInput] = useState(value?.startsWith('http') ? value : '')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function handleEmojiSelect(emoji: string) {
|
||||
onChange(emoji)
|
||||
}
|
||||
|
||||
function handleUrlChange(url: string) {
|
||||
setUrlInput(url)
|
||||
onChange(url)
|
||||
}
|
||||
|
||||
function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Limit size to 1.5MB
|
||||
if (file.size > 1.5 * 1024 * 1024) {
|
||||
alert('İkon boyutu 1.5MB\'dan küçük olmalıdır.')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
onChange(reader.result)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
onChange('')
|
||||
setUrlInput('')
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wider">
|
||||
Uygulama İkonu <span className="text-slate-500 font-normal lowercase">(isteğe bağlı)</span>
|
||||
</label>
|
||||
{value && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="text-[11px] text-rose-400 hover:text-rose-300 flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<span>İkonu Kaldır</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-2xl bg-slate-900/70 border border-slate-800 space-y-4">
|
||||
{/* Live Preview & Tabs Header */}
|
||||
<div className="flex items-center gap-4 pb-3 border-b border-slate-800/80">
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<AppIcon name={appName || 'A'} iconUrl={value} size="lg" className="ring-2 ring-emerald-500/20" />
|
||||
<span className="text-[10px] text-slate-500 font-medium">Önizleme</span>
|
||||
</div>
|
||||
|
||||
{/* Mode Switcher */}
|
||||
<div className="flex-1 flex gap-1 p-1 bg-slate-950/80 rounded-xl border border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('presets')}
|
||||
className={cn(
|
||||
'flex-1 py-1.5 px-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center gap-1.5',
|
||||
activeTab === 'presets' ? 'bg-slate-800 text-emerald-400 shadow-sm' : 'text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
<span>Hazır İkonlar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('upload')}
|
||||
className={cn(
|
||||
'flex-1 py-1.5 px-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center gap-1.5',
|
||||
activeTab === 'upload' ? 'bg-slate-800 text-emerald-400 shadow-sm' : 'text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Upload className="w-3 h-3" />
|
||||
<span>Görsel Yükle</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('url')}
|
||||
className={cn(
|
||||
'flex-1 py-1.5 px-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center gap-1.5',
|
||||
activeTab === 'url' ? 'bg-slate-800 text-emerald-400 shadow-sm' : 'text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
<LinkIcon className="w-3 h-3" />
|
||||
<span>URL</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab 1: Presets */}
|
||||
{activeTab === 'presets' && (
|
||||
<div className="grid grid-cols-8 sm:grid-cols-12 gap-1.5 animate-fade-in">
|
||||
{PRESET_EMOJIS.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => handleEmojiSelect(emoji)}
|
||||
className={cn(
|
||||
'h-9 rounded-xl flex items-center justify-center text-lg transition-all hover:scale-110 hover:bg-slate-800',
|
||||
value === emoji
|
||||
? 'bg-emerald-500/20 ring-2 ring-emerald-400'
|
||||
: 'bg-slate-900/60 border border-slate-800/60'
|
||||
)}
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Upload */}
|
||||
{activeTab === 'upload' && (
|
||||
<div className="space-y-2 animate-fade-in">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileUpload}
|
||||
accept="image/png, image/jpeg, image/webp, image/svg+xml"
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full py-4 border-2 border-dashed border-slate-700/80 hover:border-emerald-500/50 rounded-xl bg-slate-950/40 hover:bg-slate-900/40 flex flex-col items-center justify-center gap-1.5 transition-colors cursor-pointer"
|
||||
>
|
||||
<Upload className="w-5 h-5 text-emerald-400" />
|
||||
<span className="text-xs font-semibold text-slate-300">
|
||||
Görsel Seçin veya Sürükleyin
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
PNG, JPG, WebP veya SVG (Önerilen: 256x256)
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 3: URL */}
|
||||
{activeTab === 'url' && (
|
||||
<div className="space-y-2 animate-fade-in">
|
||||
<div className="flex items-center glass-input rounded-xl overflow-hidden">
|
||||
<span className="px-3 text-slate-500 text-xs border-r border-slate-800 py-2.5">
|
||||
https://
|
||||
</span>
|
||||
<input
|
||||
type="url"
|
||||
value={urlInput}
|
||||
onChange={(e) => handleUrlChange(e.target.value)}
|
||||
placeholder="ornek.com/logo.png"
|
||||
className="flex-1 bg-transparent px-3 py-2 text-white text-xs font-mono placeholder-slate-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
CDN veya harici doğrudan görsel linki yapıştırın.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Link from 'next/link'
|
||||
import { Shield, Database, ExternalLink, Code2 } from 'lucide-react'
|
||||
import { SignOutButton } from '@/components/SignOutButton'
|
||||
import { AppIcon } from './AppIcon'
|
||||
|
||||
interface NavbarProps {
|
||||
appName?: string
|
||||
appId?: string
|
||||
iconUrl?: string | null
|
||||
}
|
||||
|
||||
export function Navbar({ appName, appId, iconUrl }: NavbarProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 border-b border-slate-800/80 bg-[#080B11]/80 backdrop-blur-xl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
||||
{/* Brand & Breadcrumbs */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard" className="flex items-center gap-2.5 group">
|
||||
<div className="w-8 h-8 bg-gradient-to-tr from-emerald-600 to-emerald-400 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20 group-hover:scale-105 transition-transform">
|
||||
<Shield className="w-4 h-4 text-slate-950 stroke-[2.4]" />
|
||||
</div>
|
||||
<span className="text-white font-bold text-base tracking-tight">ConfigVault</span>
|
||||
</Link>
|
||||
|
||||
{appName && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-slate-600">/</span>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-slate-400 hover:text-slate-200 transition-colors text-xs font-medium"
|
||||
>
|
||||
Uygulamalar
|
||||
</Link>
|
||||
<span className="text-slate-600">/</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-emerald-400 font-semibold text-xs px-2 py-0.5 rounded-md bg-emerald-500/10 border border-emerald-500/20">
|
||||
<AppIcon name={appName} iconUrl={iconUrl} size="sm" className="w-4 h-4 rounded text-[10px]" />
|
||||
<span>{appName}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right tools & status */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden sm:flex items-center gap-2 px-2.5 py-1 rounded-full bg-slate-900/80 border border-slate-800 text-[11px] text-slate-400">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span>PostgreSQL Aktif</span>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-[1px] bg-slate-800 hidden sm:block" />
|
||||
|
||||
<SignOutButton />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { LogOut } from 'lucide-react'
|
||||
|
||||
export function SignOutButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-slate-400 hover:text-rose-300 hover:bg-rose-500/10 border border-transparent hover:border-rose-500/20 text-xs font-medium transition-all duration-150"
|
||||
title="Çıkış Yap"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user