feat: add github_repo field to career applications and update intern dashboard UI

This commit is contained in:
Ümit Tunç
2026-06-06 01:05:29 +03:00
parent c3f343ad70
commit 080c39ac88
5 changed files with 615 additions and 217 deletions
+101
View File
@@ -194,6 +194,107 @@ class CareerController extends Controller
return redirect()->back()->with('error', 'Dosya yüklenirken bir hata oluştu.');
}
public function saveGithubRepo(Request $request)
{
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$request->validate([
'github_repo' => 'required|string|url|max:255',
], [
'github_repo.required' => 'Lütfen Github depo URL\'sini girin.',
'github_repo.url' => 'Geçerli bir URL girilmelidir.',
'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.',
]);
$intern = CareerApplication::findOrFail(session('intern_id'));
// Sanitize & format github URL
$repoUrl = trim($request->github_repo);
$intern->update([
'github_repo' => $repoUrl
]);
return redirect()->back()->with('success', 'Github deposu başarıyla güncellendi.');
}
public function downloadMarkdown()
{
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$intern = CareerApplication::findOrFail(session('intern_id'));
$repo = $intern->github_repo;
if (!$repo) {
return redirect()->back()->with('error', 'Lütfen önce Github deposunu tanımlayın.');
}
// Parse owner and repo
// E.g., https://github.com/owner/repo or owner/repo
$repoClean = str_replace(['https://github.com/', 'http://github.com/', 'https://www.github.com/', 'http://www.github.com/'], '', $repo);
$repoClean = trim($repoClean, '/');
$parts = explode('/', $repoClean);
if (count($parts) < 2) {
return redirect()->back()->with('error', 'Geçersiz Github depo formatı.');
}
$owner = $parts[0];
$repoName = explode('.', $parts[1])[0]; // Remove .git if exists
// Fetch commits
$url = "https://api.github.com/repos/{$owner}/{$repoName}/commits?per_page=100";
$opts = [
'http' => [
'method' => 'GET',
'header' => [
'User-Agent: PHP-Github-Client',
]
]
];
$context = stream_context_create($opts);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
return redirect()->back()->with('error', 'Github API\'den commit bilgileri alınamadı. Deponuzun public olduğundan emin olun.');
}
$commits = json_decode($response, true);
if (!is_array($commits)) {
return redirect()->back()->with('error', 'Github API yanıtı çözümlenemedi.');
}
// Sort commits chronological (ascending)
$commits = array_reverse($commits);
// Generate Markdown content
$md = "# Staj Günlüğü - " . $intern->name . "\n";
$md .= "Depo: https://github.com/{$owner}/{$repoName}\n\n";
$day = 1;
foreach ($commits as $c) {
$message = $c['commit']['message'] ?? '';
$dateStr = $c['commit']['author']['date'] ?? '';
$date = $dateStr ? \Carbon\Carbon::parse($dateStr)->format('d.m.Y H:i') : '';
$hash = substr($c['sha'] ?? '', 0, 7);
$md .= "## {$day}. Gün\n";
$md .= "- **Tarih:** {$date}\n";
$md .= "- **Commit:** `{$hash}`\n";
$md .= "- **Mesaj:** {$message}\n\n";
$day++;
}
$fileName = str()->slug($intern->name) . "-staj-gunlugu.md";
return response($md, 200, [
'Content-Type' => 'text/markdown; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
]);
}
public function internLogout()
{
session()->forget('intern_id');
+1
View File
@@ -29,6 +29,7 @@ class CareerApplication extends Model
'internship_start_date',
'internship_end_date',
'internship_total_days',
'github_repo',
];
/**
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->string('github_repo')->nullable()->after('internship_total_days');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn('github_repo');
});
}
};
@@ -53,27 +53,73 @@
<!-- Application Info & Document Download Grid -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-8">
<!-- Sidebar stats / statuses -->
<!-- Sidebar stats / statuses / tabs -->
<div class="md:col-span-4 space-y-6">
<div class="bg-white rounded-3xl p-6 shadow-xl border border-slate-100/50">
<h3 class="font-bold text-slate-800 text-lg mb-4 pb-2 border-b border-slate-100">Başvuru Detayları</h3>
<div class="bg-white rounded-3xl p-4 shadow-xl border border-slate-100/50">
<h3 class="font-bold text-slate-800 text-base mb-4 px-3 pb-2 border-b border-slate-100">Menü</h3>
<div class="space-y-4">
<div class="space-y-1" role="tablist">
<!-- Menu Item 1: Kişisel Bilgileriniz -->
<button type="button" role="tab" aria-selected="true" data-tab-target="personal-info" class="tab-btn active w-full flex items-center gap-4 p-3 rounded-2xl text-left transition-all border border-transparent">
<div class="w-10 h-10 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center text-xl flex-shrink-0 tab-icon">
<i class="uil uil-user"></i>
</div>
<div class="flex-grow">
<span class="block text-sm font-extrabold text-slate-800">Kişisel Bilgiler</span>
<span class="block text-xs text-slate-400 font-semibold mt-0.5">Başvuru detaylarınız</span>
</div>
</button>
<!-- Menu Item 2: Belgeleriniz -->
<button type="button" role="tab" aria-selected="false" data-tab-target="documents" class="tab-btn w-full flex items-center gap-4 p-3 rounded-2xl text-left transition-all border border-transparent">
<div class="w-10 h-10 rounded-xl bg-slate-50 text-slate-500 flex items-center justify-center text-xl flex-shrink-0 tab-icon">
<i class="uil uil-file-alt"></i>
</div>
<div class="flex-grow">
<span class="block text-sm font-extrabold text-slate-800">Belgeleriniz</span>
<span class="block text-xs text-slate-400 font-semibold mt-0.5">CV ve staj formları</span>
</div>
</button>
<!-- Menu Item 3: Staj Günlüğü -->
<button type="button" role="tab" aria-selected="false" data-tab-target="journal" class="tab-btn w-full flex items-center gap-4 p-3 rounded-2xl text-left transition-all border border-transparent">
<div class="w-10 h-10 rounded-xl bg-slate-50 text-slate-500 flex items-center justify-center text-xl flex-shrink-0 tab-icon">
<i class="uil uil-github"></i>
</div>
<div class="flex-grow">
<span class="block text-sm font-extrabold text-slate-800">Staj Günlüğü</span>
<span class="block text-xs text-slate-400 font-semibold mt-0.5">GitHub commit geçmişi</span>
</div>
</button>
</div>
</div>
</div>
<!-- Content Panels -->
<div class="md:col-span-8">
<!-- Panel 1: Kişisel Bilgileriniz -->
<div id="personal-info-panel" class="tab-panel space-y-6">
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
<h3 class="font-bold text-slate-800 text-lg mb-6 pb-2 border-b border-slate-100">Kişisel Bilgileriniz</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<span class="text-xs text-slate-400 font-semibold block">E-posta</span>
<span class="text-sm font-semibold text-slate-700 block mt-0.5">{{ $intern->email }}</span>
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">E-posta</span>
<span class="text-sm font-bold text-slate-700 block mt-1.5">{{ $intern->email }}</span>
</div>
@if($intern->phone)
<div>
<span class="text-xs text-slate-400 font-semibold block">Telefon</span>
<span class="text-sm font-semibold text-slate-700 block mt-0.5">{{ $intern->phone }}</span>
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">Telefon</span>
<span class="text-sm font-bold text-slate-700 block mt-1.5">{{ $intern->phone }}</span>
</div>
@endif
<div>
<span class="text-xs text-slate-400 font-semibold block">Staj Durumu</span>
<span class="inline-flex mt-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">Staj Durumu</span>
<div class="mt-2">
<span class="inline-flex px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider
@if($intern->status === 'accepted') bg-green-50 text-green-700 border border-green-200
@elseif($intern->status === 'rejected') bg-red-50 text-red-700 border border-red-200
@elseif($intern->status === 'reviewed') bg-blue-50 text-blue-700 border border-blue-200
@@ -86,33 +132,34 @@
@else Beklemede @endif
</span>
</div>
</div>
@if($intern->internship_total_days)
<div>
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">Toplam Süre</span>
<span class="text-sm font-bold text-blue-600 block mt-1.5">{{ $intern->internship_total_days }} İş Günü <span class="text-xs text-slate-400 font-normal">(Haftada 5 Gün)</span></span>
</div>
@endif
@if($intern->internship_start_date)
<div class="border-t border-slate-100 pt-4">
<span class="text-xs text-slate-400 font-semibold block">Staj Başlangıç Tarihi</span>
<span class="text-sm font-semibold text-slate-700 block mt-0.5">{{ \Carbon\Carbon::parse($intern->internship_start_date)->format('d.m.Y') }}</span>
<div>
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">Staj Başlangıç Tarihi</span>
<span class="text-sm font-bold text-slate-700 block mt-1.5">{{ \Carbon\Carbon::parse($intern->internship_start_date)->format('d.m.Y') }}</span>
</div>
@endif
@if($intern->internship_end_date)
<div>
<span class="text-xs text-slate-400 font-semibold block">Staj Bitiş Tarihi</span>
<span class="text-sm font-semibold text-slate-700 block mt-0.5">{{ \Carbon\Carbon::parse($intern->internship_end_date)->format('d.m.Y') }}</span>
</div>
@endif
@if($intern->internship_total_days)
<div>
<span class="text-xs text-slate-400 font-semibold block">Toplam Süre</span>
<span class="text-sm font-bold text-blue-600 block mt-0.5">{{ $intern->internship_total_days }} İş Günü <span class="text-xs text-slate-400 font-normal">(Haftada 5 Gün)</span></span>
<span class="text-xs text-slate-400 font-extrabold uppercase tracking-wider block">Staj Bitiş Tarihi</span>
<span class="text-sm font-bold text-slate-700 block mt-1.5">{{ \Carbon\Carbon::parse($intern->internship_end_date)->format('d.m.Y') }}</span>
</div>
@endif
</div>
</div>
</div>
<!-- Document download cards -->
<div class="md:col-span-8 space-y-6">
<!-- Panel 2: Belgeleriniz -->
<div id="documents-panel" class="tab-panel hidden space-y-6">
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
<h3 class="font-bold text-slate-800 text-lg mb-6 pb-2 border-b border-slate-100">Belgeleriniz</h3>
@@ -301,6 +348,61 @@
</div>
</div>
<!-- Panel 3: Staj Günlüğü -->
<div id="journal-panel" class="tab-panel hidden space-y-6">
<div class="bg-white rounded-3xl p-6 md:p-8 shadow-xl border border-slate-100/50">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 pb-4 border-b border-slate-100">
<div>
<h3 class="font-bold text-slate-800 text-lg">Staj Günlüğü</h3>
<p class="text-xs text-slate-400 mt-0.5">Public GitHub deponuzdaki commit geçmişi günlük olarak listelenir.</p>
</div>
@if($intern->github_repo)
<a href="{{ route('intern.download-journal') }}" class="w-full sm:w-auto px-4 py-2.5 bg-green-600 hover:bg-green-700 text-white font-bold rounded-xl text-xs transition-all flex items-center justify-center gap-2 shadow-md shadow-green-500/10">
<i class="uil uil-arrow-down-tray text-sm"></i>
<span>Günlüğü İndir (.md)</span>
</a>
@endif
</div>
<!-- GitHub Repository URL Configuration -->
<div class="p-5 rounded-2xl border border-slate-100 bg-slate-50/50 mb-6">
<form action="{{ route('intern.save-github') }}" method="POST" class="space-y-4">
@csrf
<div>
<label for="github_repo" class="block text-xs font-extrabold text-slate-500 uppercase tracking-wider mb-2">GitHub Public Depo URL'si</label>
<div class="flex flex-col sm:flex-row gap-3">
<input type="url" name="github_repo" id="github_repo" placeholder="https://github.com/kullaniciadi/depo-adi" value="{{ $intern->github_repo }}" required class="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-blue-400 focus:ring-1 focus:ring-blue-400 focus:outline-none text-sm font-semibold text-slate-700 bg-white transition-all" />
<button type="submit" class="px-5 py-3 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-xl text-sm transition-all shadow-md shadow-blue-500/10 flex-shrink-0">
{{ $intern->github_repo ? 'Güncelle' : 'Kaydet' }}
</button>
</div>
<span class="block text-xs text-slate-400 mt-2"><i class="uil uil-info-circle text-blue-600"></i> Deponuzun <strong>public (herkese açık)</strong> olması ve en az bir commit içermesi gerekmektedir.</span>
</div>
</form>
</div>
<!-- Commits timeline / history load wrapper -->
<div id="commits-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-blue-600 mb-2"></div>
<span class="text-xs text-slate-400 font-semibold">GitHub'dan commitler yükleniyor...</span>
</div>
<div id="commits-error" class="hidden p-4 rounded-xl bg-red-50 border border-red-200 text-[#e31e24] text-xs font-semibold">
</div>
<div id="commits-empty" class="hidden p-6 rounded-2xl border border-slate-100 text-center text-slate-400 italic text-sm">
Henüz hiç commit bulunamadı veya deponuz tanımlanmadı. Lütfen deponuzu tanımlayın ve commitlerinizi gönderin.
</div>
<div id="commits-timeline" class="hidden relative border-l-2 border-slate-100 ml-4 pl-6 space-y-6">
<!-- Loaded dynamically by JS -->
</div>
</div>
</div>
</div>
</div>
</div>
@@ -310,6 +412,161 @@
@push('styles')
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Tab Switch Logic
document.addEventListener('DOMContentLoaded', function() {
const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanels = document.querySelectorAll('.tab-panel');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Deactivate active tab
tabButtons.forEach(btn => {
btn.classList.remove('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
btn.classList.add('bg-transparent', 'border-transparent', 'text-slate-600');
btn.querySelector('.tab-icon').classList.remove('bg-blue-600', 'text-white');
btn.querySelector('.tab-icon').classList.add('bg-slate-50', 'text-slate-500');
btn.setAttribute('aria-selected', 'false');
});
// Hide active panels
tabPanels.forEach(panel => {
panel.classList.add('hidden');
});
// Activate clicked tab
button.classList.add('active', 'bg-blue-50', 'border-blue-100', 'text-blue-900');
button.classList.remove('bg-transparent', 'border-transparent', 'text-slate-600');
button.querySelector('.tab-icon').classList.add('bg-blue-600', 'text-white');
button.querySelector('.tab-icon').classList.remove('bg-slate-50', 'text-slate-500');
button.setAttribute('aria-selected', 'true');
// Show targets panel
const targetId = button.getAttribute('data-tab-target');
const targetPanel = document.getElementById(targetId + '-panel');
if (targetPanel) {
targetPanel.classList.remove('hidden');
}
// If journal is selected, trigger GitHub commits loading
if (targetId === 'journal') {
loadGithubCommits();
}
});
});
// Handle initial state of date logic
calculateEndDateFrontend();
});
const githubRepoUrl = @json($intern->github_repo);
function loadGithubCommits() {
const loader = document.getElementById('commits-loader');
const errorEl = document.getElementById('commits-error');
const emptyEl = document.getElementById('commits-empty');
const timelineEl = document.getElementById('commits-timeline');
if (!githubRepoUrl) {
emptyEl.classList.remove('hidden');
return;
}
loader.classList.remove('hidden');
errorEl.classList.add('hidden');
emptyEl.classList.add('hidden');
timelineEl.classList.add('hidden');
timelineEl.innerHTML = '';
// 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 veya yukarıdaki indirme butonunu kullanın.');
} 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 (Day 1, Day 2...)
commits.reverse();
timelineEl.classList.remove('hidden');
commits.forEach((item, index) => {
const dayNum = index + 1;
const message = item.commit.message;
const authorDate = new Date(item.commit.author.date);
const formattedDate = authorDate.toLocaleDateString('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const shaShort = item.sha.substring(0, 7);
const commitUrl = item.html_url;
const commitHtml = `
<div class="relative pl-6 pb-6 last:pb-0">
<div class="absolute -left-[7px] top-1.5 w-3 h-3 rounded-full bg-blue-600 shadow-sm ring-4 ring-white"></div>
<div class="bg-slate-50/60 rounded-2xl p-4 border border-slate-100 hover:border-slate-200 hover:bg-slate-50 transition-all">
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<span class="text-xs uppercase font-extrabold tracking-wider text-blue-600 bg-blue-50 px-2.5 py-1 rounded-full">${dayNum}. Gün</span>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-400 font-semibold">${formattedDate}</span>
<a href="${commitUrl}" target="_blank" class="text-xs font-mono font-bold text-slate-500 hover:text-blue-600 bg-white px-2 py-0.5 rounded border border-slate-200/60 transition-colors flex items-center gap-1">
<i class="uil uil-external-link-alt text-[10px]"></i>
${shaShort}
</a>
</div>
</div>
<p class="text-sm font-bold text-slate-700 whitespace-pre-line leading-relaxed">${escapeHtml(message)}</p>
</div>
</div>
`;
timelineEl.insertAdjacentHTML('beforeend', commitHtml);
});
})
.catch(err => {
loader.classList.add('hidden');
errorEl.textContent = err.message || 'Bir hata oluştu.';
errorEl.classList.remove('hidden');
});
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function handleFileSelected(input) {
const fileNameElement = document.getElementById('selected-file-name');
const submitBtn = document.getElementById('submit-upload-btn');
@@ -366,11 +623,6 @@
endDateInput.value = `${yyyy}-${mm}-${dd}`;
}
// Trigger calculation once on load if values exist
document.addEventListener('DOMContentLoaded', function() {
calculateEndDateFrontend();
});
function copyToClipboard(elementId, btn) {
const text = document.getElementById(elementId).textContent;
navigator.clipboard.writeText(text).then(() => {
@@ -387,6 +639,20 @@
.shadow-xl {
box-shadow: 0 1rem 3rem rgba(30, 41, 59, 0.04) !important;
}
.tab-btn {
transition: all 0.2s ease-in-out;
}
.tab-btn:hover:not(.active) {
background-color: #f8fafc;
}
.tab-btn.active {
background-color: #eff6ff;
border-color: #dbeafe;
color: #1e40af !important;
}
.tab-btn.active span.text-slate-800 {
color: #1e40af !important;
}
</style>
@endpush
@endsection
+2
View File
@@ -120,6 +120,8 @@ Route::get('/stajyer/giris', [\App\Http\Controllers\CareerController::class, 'in
Route::post('/stajyer/giris', [\App\Http\Controllers\CareerController::class, 'internLogin'])->name('intern.login.submit');
Route::get('/stajyer/panel', [\App\Http\Controllers\CareerController::class, 'internDashboard'])->name('intern.dashboard');
Route::post('/stajyer/form-yukle', [\App\Http\Controllers\CareerController::class, 'uploadInternshipForm'])->name('intern.upload-form');
Route::post('/stajyer/github-kaydet', [\App\Http\Controllers\CareerController::class, 'saveGithubRepo'])->name('intern.save-github');
Route::get('/stajyer/gunluk-indir', [\App\Http\Controllers\CareerController::class, 'downloadMarkdown'])->name('intern.download-journal');
Route::post('/stajyer/cikis', [\App\Http\Controllers\CareerController::class, 'internLogout'])->name('intern.logout');
// Payment Process