refactor: organize career application form into tabs and add intern journal timeline component
This commit is contained in:
+119
-98
@@ -13,6 +13,9 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Schemas\Components\Utilities\Set;
|
use Filament\Schemas\Components\Utilities\Set;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Filament\Schemas\Components\Tabs;
|
||||||
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
|
use Filament\Schemas\Components\View;
|
||||||
|
|
||||||
class CareerApplicationForm
|
class CareerApplicationForm
|
||||||
{
|
{
|
||||||
@@ -20,108 +23,126 @@ class CareerApplicationForm
|
|||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
TextInput::make('name')
|
Tabs::make('Tabs')
|
||||||
->label(__('career.name'))
|
->tabs([
|
||||||
->required()
|
Tab::make('Kişisel ve Başvuru Bilgileri')
|
||||||
->disabled(),
|
->icon('heroicon-m-user')
|
||||||
|
->schema([
|
||||||
TextInput::make('email')
|
TextInput::make('name')
|
||||||
->label(__('career.email'))
|
->label(__('career.name'))
|
||||||
->email()
|
->required()
|
||||||
->required()
|
->disabled(),
|
||||||
->disabled(),
|
|
||||||
|
TextInput::make('email')
|
||||||
TextInput::make('phone')
|
->label(__('career.email'))
|
||||||
->label(__('career.phone'))
|
->email()
|
||||||
->disabled(),
|
->required()
|
||||||
|
->disabled(),
|
||||||
FileUpload::make('cv_path')
|
|
||||||
->label(__('career.cv'))
|
TextInput::make('phone')
|
||||||
->disk('public')
|
->label(__('career.phone'))
|
||||||
->directory('cvs')
|
->disabled(),
|
||||||
->required()
|
|
||||||
->disabled()
|
Select::make('status')
|
||||||
->downloadable(),
|
->label(__('career.status'))
|
||||||
|
->options([
|
||||||
Textarea::make('message')
|
'pending' => __('career.pending'),
|
||||||
->label(__('career.message'))
|
'reviewed' => __('career.reviewed'),
|
||||||
->disabled()
|
'rejected' => __('career.rejected'),
|
||||||
->columnSpanFull(),
|
'accepted' => __('career.accepted'),
|
||||||
|
'waiting_document' => __('career.waiting_document'),
|
||||||
Select::make('status')
|
])
|
||||||
->label(__('career.status'))
|
->required(),
|
||||||
->options([
|
|
||||||
'pending' => __('career.pending'),
|
|
||||||
'reviewed' => __('career.reviewed'),
|
|
||||||
'rejected' => __('career.rejected'),
|
|
||||||
'accepted' => __('career.accepted'),
|
|
||||||
'waiting_document' => __('career.waiting_document'),
|
|
||||||
])
|
|
||||||
->required(),
|
|
||||||
|
|
||||||
Section::make('Stajyer Giriş Bilgileri & Belgeleri')
|
Textarea::make('message')
|
||||||
->description('Stajyerin sisteme giriş yapabilmesi için kullanıcı adı, şifre belirleyin ve imzalı staj formunu yükleyin.')
|
->label(__('career.message'))
|
||||||
->collapsible()
|
->disabled()
|
||||||
->schema([
|
->columnSpanFull(),
|
||||||
TextInput::make('username')
|
])->columns(2),
|
||||||
->label('Kullanıcı Adı')
|
|
||||||
->unique(ignoreRecord: true)
|
|
||||||
->nullable(),
|
|
||||||
|
|
||||||
TextInput::make('password')
|
|
||||||
->label('Şifre')
|
|
||||||
->password()
|
|
||||||
->revealable()
|
|
||||||
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
|
|
||||||
->dehydrated(fn ($state) => filled($state))
|
|
||||||
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
|
|
||||||
->nullable()
|
|
||||||
->suffixAction(
|
|
||||||
Action::make('generatePassword')
|
|
||||||
->icon('heroicon-m-arrow-path')
|
|
||||||
->action(fn (Set $set) => $set('password', Str::random(12)))
|
|
||||||
),
|
|
||||||
|
|
||||||
FileUpload::make('to_be_signed_internship_form_path')
|
|
||||||
->label('İmzalanacak Staj Formu (Stajyerden)')
|
|
||||||
->disk('public')
|
|
||||||
->directory('to_be_signed_interns')
|
|
||||||
->downloadable()
|
|
||||||
->nullable(),
|
|
||||||
|
|
||||||
FileUpload::make('signed_internship_form_path')
|
Tab::make('Staj Belgeleri & Giriş Bilgileri')
|
||||||
->label('İmzalı Staj Formu')
|
->icon('heroicon-m-document-text')
|
||||||
->disk('public')
|
->schema([
|
||||||
->directory('signed_interns')
|
FileUpload::make('cv_path')
|
||||||
->downloadable()
|
->label(__('career.cv'))
|
||||||
->nullable(),
|
->disk('public')
|
||||||
|
->directory('cvs')
|
||||||
|
->required()
|
||||||
|
->disabled()
|
||||||
|
->downloadable(),
|
||||||
|
|
||||||
DatePicker::make('internship_start_date')
|
TextInput::make('username')
|
||||||
->label('Staj Başlangıç Tarihi')
|
->label('Kullanıcı Adı')
|
||||||
->live()
|
->unique(ignoreRecord: true)
|
||||||
->afterStateUpdated(function ($state, $get, Set $set) {
|
->nullable(),
|
||||||
if ($state && $get('internship_total_days')) {
|
|
||||||
self::calculateEndDate($state, $get('internship_total_days'), $set);
|
TextInput::make('password')
|
||||||
} elseif ($state && $get('internship_end_date')) {
|
->label('Şifre')
|
||||||
self::calculateTotalDays($state, $get('internship_end_date'), $set);
|
->password()
|
||||||
}
|
->revealable()
|
||||||
})
|
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
|
||||||
->nullable(),
|
->dehydrated(fn ($state) => filled($state))
|
||||||
|
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
|
||||||
DatePicker::make('internship_end_date')
|
->nullable()
|
||||||
->label('Staj Bitiş Tarihi')
|
->suffixAction(
|
||||||
->live()
|
Action::make('generatePassword')
|
||||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
|
->icon('heroicon-m-arrow-path')
|
||||||
->nullable(),
|
->action(fn (Set $set) => $set('password', Str::random(12)))
|
||||||
|
),
|
||||||
|
|
||||||
|
FileUpload::make('to_be_signed_internship_form_path')
|
||||||
|
->label('İmzalanacak Staj Formu (Stajyerden)')
|
||||||
|
->disk('public')
|
||||||
|
->directory('to_be_signed_interns')
|
||||||
|
->downloadable()
|
||||||
|
->nullable(),
|
||||||
|
|
||||||
TextInput::make('internship_total_days')
|
FileUpload::make('signed_internship_form_path')
|
||||||
->label('Toplam Staj Süresi (İş Günü)')
|
->label('İmzalı Staj Formu')
|
||||||
->numeric()
|
->disk('public')
|
||||||
->live()
|
->directory('signed_interns')
|
||||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
|
->downloadable()
|
||||||
->nullable(),
|
->nullable(),
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
DatePicker::make('internship_start_date')
|
||||||
|
->label('Staj Başlangıç Tarihi')
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(function ($state, $get, Set $set) {
|
||||||
|
if ($state && $get('internship_total_days')) {
|
||||||
|
self::calculateEndDate($state, $get('internship_total_days'), $set);
|
||||||
|
} elseif ($state && $get('internship_end_date')) {
|
||||||
|
self::calculateTotalDays($state, $get('internship_end_date'), $set);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->nullable(),
|
||||||
|
|
||||||
|
DatePicker::make('internship_end_date')
|
||||||
|
->label('Staj Bitiş Tarihi')
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
|
||||||
|
->nullable(),
|
||||||
|
|
||||||
|
TextInput::make('internship_total_days')
|
||||||
|
->label('Toplam Staj Süresi (İş Günü)')
|
||||||
|
->numeric()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
|
||||||
|
->nullable(),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Tab::make('Staj Günlüğü')
|
||||||
|
->icon('heroicon-m-squares-plus')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('github_repo')
|
||||||
|
->label('GitHub Depo URL\'si')
|
||||||
|
->url()
|
||||||
|
->nullable()
|
||||||
|
->live(),
|
||||||
|
|
||||||
|
// View::make('filament.components.intern-journal-timeline')
|
||||||
|
// ->columnSpanFull()
|
||||||
|
])
|
||||||
|
])->columnSpanFull()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
@php
|
||||||
|
$githubRepo = $get('github_repo') ?? ($record ? $record->github_repo : null);
|
||||||
|
$uid = 'journal_' . uniqid();
|
||||||
|
@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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.classList.remove('hidden');
|
||||||
|
errorEl.classList.add('hidden');
|
||||||
|
emptyEl.classList.add('hidden');
|
||||||
|
navEl.classList.add('hidden');
|
||||||
|
containerEl.classList.add('hidden');
|
||||||
|
commitDays = [];
|
||||||
|
|
||||||
|
// Parse owner and repo
|
||||||
|
let repoClean = 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');
|
||||||
|
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 => {
|
||||||
|
loader.classList.add('hidden');
|
||||||
|
if (!Array.isArray(commits) || commits.length === 0) {
|
||||||
|
emptyEl.classList.remove('hidden');
|
||||||
|
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;
|
||||||
|
if (dateStr) {
|
||||||
|
const dateOnly = dateStr.substring(0, 10);
|
||||||
|
if (!grouped[dateOnly]) {
|
||||||
|
grouped[dateOnly] = [];
|
||||||
|
}
|
||||||
|
grouped[dateOnly].push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedDates = Object.keys(grouped).sort();
|
||||||
|
|
||||||
|
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]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (commitDays.length === 0) {
|
||||||
|
emptyEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeDayIndex = 0;
|
||||||
|
navEl.classList.remove('hidden');
|
||||||
|
containerEl.classList.remove('hidden');
|
||||||
|
|
||||||
|
renderDayTabs();
|
||||||
|
renderActiveDay();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
loader.classList.add('hidden');
|
||||||
|
errorEl.textContent = err.message || 'Bir hata oluştu.';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTab = container.children[activeDayIndex];
|
||||||
|
if (activeTab) {
|
||||||
|
activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<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`;
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user