refactor: replace inline journal timeline view with a dedicated Livewire component for improved performance and structure

This commit is contained in:
Ümit Tunç
2026-06-06 01:30:15 +03:00
parent 0d3a514456
commit b776ebbd26
4 changed files with 341 additions and 215 deletions
@@ -15,7 +15,7 @@ use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Str;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\View;
use Filament\Schemas\Components\Livewire;
class CareerApplicationForm
{
@@ -139,8 +139,8 @@ class CareerApplicationForm
->nullable()
->live(),
// View::make('filament.components.intern-journal-timeline')
// ->columnSpanFull()
Livewire::make(\App\Livewire\InternJournalTimeline::class)
->columnSpanFull()
])
])->columnSpanFull()
]);
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Database\Eloquent\Model;
class InternJournalTimeline extends Component
{
public ?Model $record = null;
public function getGithubRepoProperty(): ?string
{
return $this->record?->github_repo;
}
public function render()
{
return view('livewire.intern-journal-timeline');
}
}
@@ -1,86 +1,50 @@
@php
$githubRepo = $get('github_repo') ?? ($record ? $record->github_repo : null);
$uid = 'journal_' . uniqid();
$githubRepo = $get('github_repo') ?? ($getRecord ? $getRecord()->github_repo : null);
@endphp
<div id="{{ $uid }}" class="mt-4">
<!-- Loader -->
<div id="{{ $uid }}-loader" class="hidden py-8 flex flex-col items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mb-2"></div>
<span class="text-xs text-gray-500 font-semibold">GitHub'dan commitler yükleniyor...</span>
</div>
<div
x-data="{
githubRepoUrl: @js($githubRepo),
commitDays: [],
activeDayIndex: 0,
loading: false,
errorMsg: '',
isEmpty: false,
<!-- Error Alert -->
<div id="{{ $uid }}-error" class="hidden p-4 rounded-xl bg-danger-50 border border-danger-200 text-danger-600 text-xs font-semibold">
</div>
init() {
this.loadCommits();
},
<!-- Empty State -->
<div id="{{ $uid }}-empty" class="hidden p-6 rounded-2xl border border-gray-150 text-center text-gray-400 italic text-sm">
Henüz hiç commit bulunamadı veya stajyerin deponuz tanımlanmadı.
</div>
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
},
<!-- Day Paginator / Navbar -->
<div id="{{ $uid }}-carousel-nav" class="hidden flex items-center justify-between gap-4 p-3 bg-gray-50 border border-gray-100 rounded-2xl mb-6 select-none">
<button type="button" id="{{ $uid }}-prev-day-btn" onclick="{{ $uid }}_navigateDay(-1)" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<div class="flex-grow overflow-x-auto no-scrollbar py-1">
<div id="{{ $uid }}-tabs-container" class="flex gap-2 justify-start sm:justify-center min-w-max px-2">
<!-- Day tabs rendered dynamically -->
</div>
</div>
<button type="button" id="{{ $uid }}-next-day-btn" onclick="{{ $uid }}_navigateDay(1)" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
<!-- Run on init script -->
<script>
(function() {
const uid = '{{ $uid }}';
const githubRepoUrl = @json($githubRepo);
let commitDays = [];
let activeDayIndex = 0;
window[uid + '_loadGithubCommits'] = function() {
const loader = document.getElementById(uid + '-loader');
const errorEl = document.getElementById(uid + '-error');
const emptyEl = document.getElementById(uid + '-empty');
const navEl = document.getElementById(uid + '-carousel-nav');
const containerEl = document.getElementById(uid + '-timeline-container');
if (!githubRepoUrl) {
emptyEl.classList.remove('hidden');
loadCommits() {
if (!this.githubRepoUrl) {
this.isEmpty = true;
return;
}
loader.classList.remove('hidden');
errorEl.classList.add('hidden');
emptyEl.classList.add('hidden');
navEl.classList.add('hidden');
containerEl.classList.add('hidden');
commitDays = [];
this.loading = true;
this.errorMsg = '';
this.isEmpty = false;
this.commitDays = [];
// Parse owner and repo
let repoClean = githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
let repoClean = this.githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
repoClean = repoClean.replace(/\/$/, '');
const parts = repoClean.split('/');
if (parts.length < 2) {
loader.classList.add('hidden');
errorEl.textContent = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
errorEl.classList.remove('hidden');
this.loading = false;
this.errorMsg = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
return;
}
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=100`)
fetch('https://api.github.com/repos/' + owner + '/' + repo + '/commits?per_page=100')
.then(response => {
if (!response.ok) {
if (response.status === 404) {
@@ -94,16 +58,14 @@
return response.json();
})
.then(commits => {
loader.classList.add('hidden');
this.loading = false;
if (!Array.isArray(commits) || commits.length === 0) {
emptyEl.classList.remove('hidden');
this.isEmpty = true;
return;
}
// Reverse to show in chronological order
commits.reverse();
// Group commits by date (YYYY-MM-DD)
const grouped = {};
commits.forEach(item => {
const dateStr = item.commit.author.date;
@@ -118,171 +80,133 @@
const sortedDates = Object.keys(grouped).sort();
commitDays = sortedDates.map((date, index) => {
this.commitDays = sortedDates.map((date, index) => {
const dateParts = date.split('-');
return {
date: date,
dayNum: index + 1,
formattedDate: `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}`,
commits: grouped[date]
formattedDate: dateParts[2] + '.' + dateParts[1] + '.' + dateParts[0],
commits: grouped[date].map(c => ({
message: c.commit.message,
time: new Date(c.commit.author.date).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }),
sha: c.sha.substring(0, 7),
url: c.html_url
}))
};
});
if (commitDays.length === 0) {
emptyEl.classList.remove('hidden');
if (this.commitDays.length === 0) {
this.isEmpty = true;
return;
}
activeDayIndex = 0;
navEl.classList.remove('hidden');
containerEl.classList.remove('hidden');
renderDayTabs();
renderActiveDay();
this.activeDayIndex = 0;
})
.catch(err => {
loader.classList.add('hidden');
errorEl.textContent = err.message || 'Bir hata oluştu.';
errorEl.classList.remove('hidden');
this.loading = false;
this.errorMsg = err.message || 'Bir hata oluştu.';
});
}
},
function renderDayTabs() {
const container = document.getElementById(uid + '-tabs-container');
container.innerHTML = '';
commitDays.forEach((day, index) => {
const isActive = index === activeDayIndex;
const activeClass = isActive
? 'text-primary-700 bg-primary-50 border border-primary-200 font-extrabold shadow-sm'
: 'bg-white hover:bg-gray-50 text-gray-600 border border-gray-200 font-semibold hover:text-primary-600';
const tab = document.createElement('button');
tab.type = 'button';
tab.className = `px-5 py-2 rounded-xl transition-all flex flex-col items-center justify-center ${activeClass}`;
tab.innerHTML = `
<span class="text-xs">${day.dayNum}. Gün</span>
<span class="text-[9px] mt-0.5 opacity-70 font-medium">${day.formattedDate}</span>
`;
tab.onclick = () => {
activeDayIndex = index;
renderDayTabs();
renderActiveDay();
};
container.appendChild(tab);
});
get activeDay() {
return this.commitDays[this.activeDayIndex] || null;
},
const activeTab = container.children[activeDayIndex];
if (activeTab) {
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
navigateDay(dir) {
const newIndex = this.activeDayIndex + dir;
if (newIndex >= 0 && newIndex < this.commitDays.length) {
this.activeDayIndex = newIndex;
}
},
const prevBtn = document.getElementById(uid + '-prev-day-btn');
const nextBtn = document.getElementById(uid + '-next-day-btn');
prevBtn.disabled = activeDayIndex === 0;
prevBtn.classList.toggle('opacity-40', activeDayIndex === 0);
prevBtn.classList.toggle('cursor-not-allowed', activeDayIndex === 0);
nextBtn.disabled = activeDayIndex === commitDays.length - 1;
nextBtn.classList.toggle('opacity-40', activeDayIndex === commitDays.length - 1);
nextBtn.classList.toggle('cursor-not-allowed', activeDayIndex === commitDays.length - 1);
selectDay(index) {
this.activeDayIndex = index;
}
}"
class="mt-4"
>
{{-- Loader --}}
<div x-show="loading" x-cloak class="py-8 flex flex-col items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mb-2"></div>
<span class="text-xs text-gray-500 font-semibold">GitHub'dan commitler yükleniyor...</span>
</div>
function renderActiveDay() {
const card = document.getElementById(uid + '-active-day-card');
card.style.opacity = '0.3';
card.style.transform = 'translateY(5px)';
setTimeout(() => {
const day = commitDays[activeDayIndex];
document.getElementById(uid + '-active-day-title').textContent = `${day.dayNum}. Gün`;
document.getElementById(uid + '-active-day-date').innerHTML = `
{{-- Error --}}
<div x-show="errorMsg" x-cloak class="p-4 rounded-xl bg-danger-50 border border-danger-200 text-danger-600 text-xs font-semibold" x-text="errorMsg"></div>
{{-- Empty --}}
<div x-show="isEmpty && !loading" x-cloak class="p-6 rounded-2xl border border-gray-200 text-center text-gray-400 italic text-sm">
Henüz hiç commit bulunamadı veya stajyerin deposu tanımlanmadı.
</div>
{{-- Day Paginator --}}
<div x-show="commitDays.length > 0" x-cloak class="flex items-center justify-between gap-4 p-3 bg-gray-50 border border-gray-100 rounded-2xl mb-6 select-none">
<button type="button" @click="navigateDay(-1)" :disabled="activeDayIndex === 0" :class="{ 'opacity-40 cursor-not-allowed': activeDayIndex === 0 }" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<div class="flex-grow overflow-x-auto py-1" style="-ms-overflow-style: none; scrollbar-width: none;">
<div class="flex gap-2 justify-start sm:justify-center min-w-max px-2">
<template x-for="(day, index) in commitDays" :key="day.date">
<button type="button" @click="selectDay(index)" :class="index === activeDayIndex ? 'text-primary-700 bg-primary-50 border-primary-200 font-extrabold shadow-sm' : 'bg-white hover:bg-gray-50 text-gray-600 border-gray-200 font-semibold hover:text-primary-600'" class="px-5 py-2 rounded-xl transition-all flex flex-col items-center justify-center border">
<span class="text-xs" x-text="day.dayNum + '. Gün'"></span>
<span class="text-[9px] mt-0.5 opacity-70 font-medium" x-text="day.formattedDate"></span>
</button>
</template>
</div>
</div>
<button type="button" @click="navigateDay(1)" :disabled="activeDayIndex === commitDays.length - 1" :class="{ 'opacity-40 cursor-not-allowed': activeDayIndex === commitDays.length - 1 }" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>
{{-- Active Day Card --}}
<div x-show="activeDay" x-cloak class="bg-white dark:bg-gray-950 border border-gray-100 dark:border-gray-800 rounded-2xl p-6 shadow-sm">
{{-- Day Header --}}
<div class="flex items-center justify-between mb-8 pb-4 border-b border-gray-100 dark:border-gray-800">
<div class="flex items-center gap-3">
<span class="text-base font-extrabold text-primary-600 bg-primary-50 dark:bg-primary-950/20 px-4 py-1.5 rounded-full" x-text="activeDay ? activeDay.dayNum + '. Gün' : ''"></span>
<span class="text-sm font-semibold text-gray-400 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
</svg>
<span>${day.formattedDate}</span>
`;
document.getElementById(uid + '-active-day-commit-count').textContent = `${day.commits.length} Commit`;
<span x-text="activeDay ? activeDay.formattedDate : ''"></span>
</span>
</div>
<span class="text-xs font-bold text-gray-400 bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-800 px-2.5 py-1 rounded-lg" x-text="activeDay ? activeDay.commits.length + ' Commit' : ''"></span>
</div>
const eventsContainer = document.getElementById(uid + '-timeline-events');
eventsContainer.innerHTML = '';
day.commits.forEach(c => {
const message = c.commit.message;
const authorDate = new Date(c.commit.author.date);
const timeStr = authorDate.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
const shaShort = c.sha.substring(0, 7);
const commitUrl = c.html_url;
const eventHtml = `
<div class="relative group">
<!-- Timeline Dot -->
<div class="absolute -left-[31px] sm:-left-[39px] top-1.5 w-6 h-6 rounded-full bg-white border-2 border-primary-500 flex items-center justify-center transition-all group-hover:bg-primary-500">
<div class="w-2 h-2 rounded-full bg-primary-500 group-hover:bg-white transition-all"></div>
</div>
<!-- Event Card -->
<div class="p-4 bg-gray-50/50 hover:bg-primary-50/10 border border-gray-150 hover:border-primary-100 rounded-2xl transition-all shadow-sm">
<div class="flex items-center justify-between gap-4 mb-2">
<span class="text-xs font-bold text-gray-450 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>${timeStr}</span>
</span>
<a href="${commitUrl}" target="_blank" class="text-xs font-mono font-bold text-primary-600 hover:text-primary-700 bg-primary-50 hover:bg-primary-100 px-2.5 py-0.5 rounded-lg border border-primary-100 transition-colors">
${shaShort}
</a>
</div>
<p class="text-sm font-bold text-gray-750 leading-relaxed whitespace-pre-line">${escapeHtml(message)}</p>
</div>
{{-- Timeline --}}
<div class="relative pl-6 sm:pl-8" style="position: relative;">
<div style="content: ''; position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background-color: rgb(241 245 249);"></div>
<div class="space-y-6">
<template x-for="(commit, ci) in (activeDay ? activeDay.commits : [])" :key="ci">
<div class="relative group">
{{-- Timeline Dot --}}
<div class="absolute top-1.5 w-6 h-6 rounded-full bg-white border-2 border-primary-500 flex items-center justify-center transition-all group-hover:bg-primary-500" style="left: -31px;">
<div class="w-2 h-2 rounded-full bg-primary-500 group-hover:bg-white transition-all"></div>
</div>
`;
eventsContainer.insertAdjacentHTML('beforeend', eventHtml);
});
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, 150);
}
window[uid + '_navigateDay'] = function(dir) {
const newIndex = activeDayIndex + dir;
if (newIndex >= 0 && newIndex < commitDays.length) {
activeDayIndex = newIndex;
renderDayTabs();
renderActiveDay();
}
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Run on init
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', window[uid + '_loadGithubCommits']);
} else {
window[uid + '_loadGithubCommits']();
}
})();
</script>
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
{{-- Event Card --}}
<div class="p-4 bg-gray-50/50 hover:bg-primary-50/10 border border-gray-200 hover:border-primary-100 rounded-2xl transition-all shadow-sm">
<div class="flex items-center justify-between gap-4 mb-2">
<span class="text-xs font-bold text-gray-400 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span x-text="commit.time"></span>
</span>
<a :href="commit.url" target="_blank" class="text-xs font-mono font-bold text-primary-600 hover:text-primary-700 bg-primary-50 hover:bg-primary-100 px-2.5 py-0.5 rounded-lg border border-primary-100 transition-colors" x-text="commit.sha"></a>
</div>
<p class="text-sm font-bold text-gray-700 leading-relaxed whitespace-pre-line" x-text="commit.message"></p>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
@@ -0,0 +1,181 @@
<div
x-data="{
githubRepoUrl: @js($this->githubRepo),
commitDays: [],
activeDayIndex: 0,
loading: false,
errorMsg: '',
isEmpty: false,
init() {
this.loadCommits();
},
loadCommits() {
if (!this.githubRepoUrl) {
this.isEmpty = true;
return;
}
this.loading = true;
this.errorMsg = '';
this.isEmpty = false;
this.commitDays = [];
let repoClean = this.githubRepoUrl.replace(/https?:\/\/(www\.)?github\.com\//i, '');
repoClean = repoClean.replace(/\/$/, '');
const parts = repoClean.split('/');
if (parts.length < 2) {
this.loading = false;
this.errorMsg = 'Github depo URL\'si çözümlenemedi. Geçerli format: https://github.com/kullanici/depo';
return;
}
const owner = parts[0];
const repo = parts[1].replace(/\.git$/i, '');
fetch('https://api.github.com/repos/' + owner + '/' + repo + '/commits?per_page=100')
.then(response => {
if (!response.ok) {
if (response.status === 404) throw new Error('Depo bulunamadı veya private (gizli). Deponuzun public (herkese açık) olduğundan emin olun.');
else if (response.status === 403) throw new Error('GitHub API istek limiti aşıldı. Lütfen daha sonra tekrar deneyin.');
else throw new Error('GitHub API hatası: ' + response.statusText);
}
return response.json();
})
.then(commits => {
this.loading = false;
if (!Array.isArray(commits) || commits.length === 0) {
this.isEmpty = true;
return;
}
commits.reverse();
const grouped = {};
commits.forEach(item => {
const dateStr = item.commit.author.date;
if (dateStr) {
const dateOnly = dateStr.substring(0, 10);
if (!grouped[dateOnly]) grouped[dateOnly] = [];
grouped[dateOnly].push(item);
}
});
const sortedDates = Object.keys(grouped).sort();
this.commitDays = sortedDates.map((date, index) => {
const dp = date.split('-');
return {
date: date,
dayNum: index + 1,
formattedDate: dp[2] + '.' + dp[1] + '.' + dp[0],
commits: grouped[date].map(c => ({
message: c.commit.message,
time: new Date(c.commit.author.date).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }),
sha: c.sha.substring(0, 7),
url: c.html_url
}))
};
});
if (this.commitDays.length === 0) {
this.isEmpty = true;
return;
}
this.activeDayIndex = 0;
})
.catch(err => {
this.loading = false;
this.errorMsg = err.message || 'Bir hata oluştu.';
});
},
get activeDay() {
return this.commitDays[this.activeDayIndex] || null;
},
navigateDay(dir) {
const n = this.activeDayIndex + dir;
if (n >= 0 && n < this.commitDays.length) this.activeDayIndex = n;
},
selectDay(i) {
this.activeDayIndex = i;
}
}"
class="mt-4"
>
{{-- Loader --}}
<div x-show="loading" x-cloak class="py-8 flex flex-col items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mb-2"></div>
<span class="text-xs text-gray-500 font-semibold">GitHub'dan commitler yükleniyor...</span>
</div>
{{-- Error --}}
<div x-show="errorMsg" x-cloak class="p-4 rounded-xl bg-danger-50 border border-danger-200 text-danger-600 text-xs font-semibold" x-text="errorMsg"></div>
{{-- Empty --}}
<div x-show="isEmpty && !loading" x-cloak class="p-6 rounded-2xl border border-gray-200 text-center text-gray-400 italic text-sm">
Henüz hiç commit bulunamadı veya stajyerin deposu tanımlanmadı.
</div>
{{-- Day Paginator --}}
<div x-show="commitDays.length > 0" x-cloak class="flex items-center justify-between gap-4 p-3 bg-gray-50 border border-gray-100 rounded-2xl mb-6 select-none">
<button type="button" @click="navigateDay(-1)" :disabled="activeDayIndex === 0" :class="{ 'opacity-40 cursor-not-allowed': activeDayIndex === 0 }" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" /></svg>
</button>
<div class="flex-grow overflow-x-auto py-1" style="-ms-overflow-style: none; scrollbar-width: none;">
<div class="flex gap-2 justify-start sm:justify-center min-w-max px-2">
<template x-for="(day, index) in commitDays" :key="day.date">
<button type="button" @click="selectDay(index)" :class="index === activeDayIndex ? 'text-primary-700 bg-primary-50 border-primary-200 font-extrabold shadow-sm' : 'bg-white hover:bg-gray-50 text-gray-600 border-gray-200 font-semibold hover:text-primary-600'" class="px-5 py-2 rounded-xl transition-all flex flex-col items-center justify-center border">
<span class="text-xs" x-text="day.dayNum + '. Gün'"></span>
<span class="text-[9px] mt-0.5 opacity-70 font-medium" x-text="day.formattedDate"></span>
</button>
</template>
</div>
</div>
<button type="button" @click="navigateDay(1)" :disabled="activeDayIndex === commitDays.length - 1" :class="{ 'opacity-40 cursor-not-allowed': activeDayIndex === commitDays.length - 1 }" class="w-10 h-10 rounded-xl bg-white hover:bg-gray-100 text-gray-600 border border-gray-200 flex items-center justify-center transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" /></svg>
</button>
</div>
{{-- Active Day Card --}}
<div x-show="activeDay" x-cloak class="bg-white dark:bg-gray-950 border border-gray-100 dark:border-gray-800 rounded-2xl p-6 shadow-sm">
<div class="flex items-center justify-between mb-8 pb-4 border-b border-gray-100 dark:border-gray-800">
<div class="flex items-center gap-3">
<span class="text-base font-extrabold text-primary-600 bg-primary-50 dark:bg-primary-950/20 px-4 py-1.5 rounded-full" x-text="activeDay ? activeDay.dayNum + '. Gün' : ''"></span>
<span class="text-sm font-semibold text-gray-400 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 mr-1"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" /></svg>
<span x-text="activeDay ? activeDay.formattedDate : ''"></span>
</span>
</div>
<span class="text-xs font-bold text-gray-400 bg-gray-50 dark:bg-gray-900 border border-gray-100 dark:border-gray-800 px-2.5 py-1 rounded-lg" x-text="activeDay ? activeDay.commits.length + ' Commit' : ''"></span>
</div>
{{-- Timeline --}}
<div class="relative pl-6 sm:pl-8">
<div style="position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background-color: rgb(229, 231, 235);"></div>
<div class="space-y-6">
<template x-for="(commit, ci) in (activeDay ? activeDay.commits : [])" :key="ci">
<div class="relative group">
<div class="absolute top-1.5 w-6 h-6 rounded-full bg-white border-2 border-primary-500 flex items-center justify-center transition-all group-hover:bg-primary-500" style="left: -31px;">
<div class="w-2 h-2 rounded-full bg-primary-500 group-hover:bg-white transition-all"></div>
</div>
<div class="p-4 bg-gray-50/50 hover:bg-primary-50/10 border border-gray-200 hover:border-primary-100 rounded-2xl transition-all shadow-sm">
<div class="flex items-center justify-between gap-4 mb-2">
<span class="text-xs font-bold text-gray-400 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span x-text="commit.time"></span>
</span>
<a :href="commit.url" target="_blank" class="text-xs font-mono font-bold text-primary-600 hover:text-primary-700 bg-primary-50 hover:bg-primary-100 px-2.5 py-0.5 rounded-lg border border-primary-100 transition-colors" x-text="commit.sha"></a>
</div>
<p class="text-sm font-bold text-gray-700 leading-relaxed whitespace-pre-line" x-text="commit.message"></p>
</div>
</div>
</template>
</div>
</div>
</div>
</div>