penambahan sertifikat

This commit is contained in:
Baghiz Zuhdi Adzin 2026-01-23 15:10:51 +07:00
parent 51a5631b74
commit 40f7a8e3c2
7 changed files with 807 additions and 120 deletions

View File

@ -137,7 +137,7 @@ class PlayerController extends Controller
if ($certificate->count() == 0) { if ($certificate->count() == 0) {
$certificate_data['user_id'] = auth()->user()->id; $certificate_data['user_id'] = auth()->user()->id;
$certificate_data['course_id'] = $request->course_id; $certificate_data['course_id'] = $request->course_id;
$certificate_data['identifier'] = random(12); $certificate_data['identifier'] = $this->generateIdentifier(12);
$certificate_data['created_at'] = date('Y-m-d H:i:s'); $certificate_data['created_at'] = date('Y-m-d H:i:s');
Certificate::insert($certificate_data); Certificate::insert($certificate_data);
} }
@ -146,6 +146,22 @@ class PlayerController extends Controller
return redirect()->back(); return redirect()->back();
} }
private function generateIdentifier($length = 12)
{
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
public function prepend_watermark() public function prepend_watermark()
{ {
return view('course_player.watermark'); return view('course_player.watermark');

View File

@ -60,6 +60,8 @@ class HomeController extends Controller
$page_data['certificate'] = $certificate->first(); $page_data['certificate'] = $certificate->first();
$page_data['qrcode'] = $qrcode; $page_data['qrcode'] = $qrcode;
// dd($qr_code_content_value);
return view('curriculum.certificate.download', $page_data); return view('curriculum.certificate.download', $page_data);
} else { } else {
return redirect(route('home'))->with('error', get_phrase('Certificate not found at this url')); return redirect(route('home'))->with('error', get_phrase('Certificate not found at this url'));

View File

@ -208,7 +208,7 @@ class QuizController extends Controller
Certificate::create([ Certificate::create([
'user_id' => $student_id, 'user_id' => $student_id,
'course_id' => $course_id, 'course_id' => $course_id,
'identifier' => substr(md5(uniqid(mt_rand(), true)), 0, 10), // Generate random ID 'identifier' => $this->generateIdentifier(12),
]); ]);
} }
} }
@ -216,6 +216,18 @@ class QuizController extends Controller
return true; return true;
} }
private function generateIdentifier($length = 12)
{
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
public function load_result(Request $request) public function load_result(Request $request)
{ {
$page_data['quiz'] = Lesson::where('id', $request->quiz_id)->first(); $page_data['quiz'] = Lesson::where('id', $request->quiz_id)->first();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -15,20 +15,31 @@
<link rel="stylesheet" type="text/css" href="{{ asset('assets/backend/css/style.css') }}"> <link rel="stylesheet" type="text/css" href="{{ asset('assets/backend/css/style.css') }}">
{{-- Font Awesome untuk ikon tambahan --}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script type="text/javascript" src="{{ asset('assets/backend/js/jquery-3.7.1.min.js') }}"></script> <script type="text/javascript" src="{{ asset('assets/backend/js/jquery-3.7.1.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('assets/global/jquery-ui-1.13.2/jquery-ui.min.js') }}"></script> <script type="text/javascript" src="{{ asset('assets/global/jquery-ui-1.13.2/jquery-ui.min.js') }}"></script>
<script src="{{ asset('assets/backend/vendors/bootstrap/bootstrap.bundle.min.js') }}"></script> <script src="{{ asset('assets/backend/vendors/bootstrap/bootstrap.bundle.min.js') }}"></script>
<style type="text/css"> <style type="text/css">
body { body {
font-family: 'Roboto', sans-serif; font-family: 'Inter', 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
} }
.draggable { .draggable {
border: 2px dashed rgb(255, 255, 255); border: 2px dashed rgba(255, 255, 255, 0.8);
cursor: move; cursor: move;
background-color: #15b57e33; background-color: #15b57e33;
top: 0; top: 0;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.draggable:hover {
border-color: #15b57e;
box-shadow: 0 6px 12px rgba(21, 181, 126, 0.2);
} }
.hidden-position:not(.certificate-layout-module) { .hidden-position:not(.certificate-layout-module) {
@ -48,17 +59,20 @@
.certificate-layout-module { .certificate-layout-module {
background-color: #fff; background-color: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
} }
.sidebar { .sidebar {
position: fixed; position: fixed;
top: 0; top: 0;
right: -300px; right: -350px;
bottom: 0; bottom: 0;
z-index: 200; z-index: 200;
background-color: #ffffff; background-color: #ffffff;
width: 300px; width: 350px;
height: 100%; height: 100%;
transition: right 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
box-shadow: -5px 0 25px rgba(0, 0, 0, 0.1);
} }
.sidebar.open { .sidebar.open {
@ -67,26 +81,49 @@
.sidebar-header { .sidebar-header {
width: 100%; width: 100%;
padding: 10px; padding: 15px;
background: rgba(0, 17, 81, 1);
color: white;
} }
.sidebar-toggle { .sidebar-toggle {
position: fixed; position: fixed;
top: 10px; top: 20px;
right: 10px; right: 20px;
z-index: 150; z-index: 150;
background: rgba(0, 17, 81, 1);
padding: 10px;
border-radius: 50%;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: transform 0.3s ease;
}
.sidebar-toggle:hover {
transform: scale(1.1);
} }
.remove-item { .remove-item {
position: absolute; position: absolute;
top: -20px; top: -10px;
right: -17px; right: -10px;
background-color: white; background-color: #dc3545;
color: white;
border-radius: 50%; border-radius: 50%;
padding: 2px; padding: 4px;
height: 20px; height: 24px;
width: 20px; width: 24px;
font-size: 16px; font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
}
.remove-item:hover {
background-color: #c82333;
transform: scale(1.1);
} }
i:not(.fas, .fa, .fab) { i:not(.fas, .fa, .fab) {
@ -96,23 +133,18 @@
} }
.dotted-background { .dotted-background {
width: 200px;
height: 200px;
background-image: radial-gradient(circle, #afafaf 1px, transparent 1px); background-image: radial-gradient(circle, #afafaf 1px, transparent 1px);
background-size: 10px 10px; background-size: 20px 20px;
/* Adjust size of dots as needed */
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 100; z-index: 100;
background-color: #e4e5ff; background-color: #f0f2f5;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-left: 10px; padding: 30px;
padding-top: 10px;
} }
.cursor-pointer { .cursor-pointer {
@ -120,97 +152,401 @@
} }
.sidebar-body { .sidebar-body {
height: 100%; height: calc(100% - 60px);
overflow-y: auto; overflow-y: auto;
padding: 20px;
}
.font-family-option {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #e0e0e0;
transition: all 0.2s ease;
cursor: pointer;
}
.font-family-option:hover {
background-color: #f8f9fa;
border-color: #667eea;
}
.font-family-option.active {
background-color: #e8eeff;
border-color: #667eea;
}
.font-family-preview {
font-size: 14px;
margin-left: 8px;
}
.badge {
margin: 2px;
padding: 6px 10px;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
transition: all 0.2s ease;
cursor: pointer;
}
.badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.form-control, .form-select {
border-radius: 8px;
border: 1px solid #dee2e6;
padding: 10px 15px;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-ol-btn-light-primary {
background-color: #e8eeff;
color: #667eea;
border: 1px solid #c2d0ff;
}
.btn-ol-btn-light-primary:hover {
background-color: #d5deff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.btn-ol-btn-primary {
background: rgba(0, 17, 81, 1);
color: white;
border: none;
}
.btn-ol-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.font-style-options {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.font-style-btn {
flex: 1;
padding: 8px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.font-style-btn:hover {
border-color: #667eea;
background-color: #f8f9fa;
}
.font-style-btn.active {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.color-picker-container {
position: relative;
}
.color-preview {
width: 30px;
height: 30px;
border-radius: 4px;
border: 1px solid #dee2e6;
cursor: pointer;
}
.font-weight-slider {
width: 100%;
margin: 10px 0;
}
.text-decoration-options {
display: flex;
gap: 10px;
}
.text-decoration-btn {
flex: 1;
padding: 8px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.text-decoration-btn:hover {
border-color: #667eea;
background-color: #f8f9fa;
}
.text-decoration-btn.active {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.font-option-group {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.font-option-title {
font-size: 14px;
font-weight: 600;
color: #495057;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
} }
</style> </style>
</head> </head>
<body> <body>
<a onclick="$('.sidebar').addClass('open')" href="#" class="sidebar-toggle"> <a onclick="$('.sidebar').addClass('open')" href="#" class="sidebar-toggle">
<svg width="40px" height="40px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <i class="fas fa-sliders-h" style="color: white; font-size: 20px;"></i>
<g id="SVGRepo_bgCarrier" stroke-width="0" />
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" />
<g id="SVGRepo_iconCarrier">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 5C3.44772 5 3 5.44772 3 6C3 6.55228 3.44772 7 4 7H20C20.5523 7 21 6.55228 21 6C21 5.44772 20.5523 5 20 5H4ZM7 12C7 11.4477 7.44772 11 8 11H20C20.5523 11 21 11.4477 21 12C21 12.5523 20.5523 13 20 13H8C7.44772 13 7 12.5523 7 12ZM13 18C13 17.4477 13.4477 17 14 17H20C20.5523 17 21 17.4477 21 18C21 18.5523 20.5523 19 20 19H14C13.4477 19 13 18.5523 13 18Z" fill="#000000" />
</g>
</svg>
</a> </a>
<div class="sidebar open"> <div class="sidebar open">
<div class="sidebar-header border-bottom d-flex align-items-center"> <div class="sidebar-header border-bottom d-flex align-items-center">
<a class="btn" href="#" onclick="$('.sidebar').removeClass('open')"> <a class="btn text-white" href="#" onclick="$('.sidebar').removeClass('open')">
<i class="fi-rr-cross-small"></i> <i class="fas fa-chevron-right"></i>
</a>
<span class="ms-2 fw-bold">{{ get_phrase('Certificate Designer') }}</span>
<a class="ms-auto btn btn-sm btn-outline-light" href="{{ route('admin.certificate.settings') }}">
<i class="fas fa-arrow-left me-1"></i>{{ get_phrase('Back') }}
</a> </a>
{{ get_phrase('Certificate elements') }}
<a class="ms-auto" href="{{ route('admin.certificate.settings') }}">{{ get_phrase('Back') }}</a>
</div> </div>
<div class="sidebar-body"> <div class="sidebar-body">
<div class="card border-0 m-2"> <div class="card border-0 shadow-sm mb-4">
<div class="card-body pt-0"> <div class="card-body">
<h6 class="card-title mt-3">{{ get_phrase('Available Variable Data') }}</h6> <h6 class="card-title d-flex align-items-center">
<span class="badge bg-secondary rounded-1">{course_duration}</span> <i class="fas fa-code me-2"></i>{{ get_phrase('Available Variables') }}
<span class="badge bg-secondary rounded-1">{instructor_name}</span> </h6>
<span class="badge bg-secondary rounded-1">{student_name}</span> <div class="d-flex flex-wrap">
<span class="badge bg-secondary rounded-1">{course_title}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_duration}')">{course_duration}</span>
<span class="badge bg-secondary rounded-1">{number_of_lesson}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{certificate_id}')">{certificate_id}</span>
<span class="badge bg-secondary rounded-1">{qr_code}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{instructor_name}')">{instructor_name}</span>
<span class="badge bg-secondary rounded-1">{course_completion_date}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{student_name}')">{student_name}</span>
<span class="badge bg-secondary rounded-1">{certificate_download_date}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_title}')">{course_title}</span>
<span class="badge bg-secondary rounded-1">{course_level}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{number_of_lesson}')">{number_of_lesson}</span>
<span class="badge bg-secondary rounded-1">{course_language}</span> <span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{qr_code}')">{qr_code}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_completion_date}')">{course_completion_date}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{certificate_download_date}')">{certificate_download_date}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_level}')">{course_level}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_language}')">{course_language}</span>
</div>
</div> </div>
</div> </div>
<div class="card border-0 m-2" id="custom_elem_form">
<div class="card-body pt-2"> <div class="card border-0 shadow-sm" id="custom_elem_form">
<h6 class="card-title">{{ get_phrase('Add a new element') }}</h6> <div class="card-body">
<h6 class="card-title d-flex align-items-center">
<i class="fas fa-plus-circle me-2"></i>{{ get_phrase('Add New Element') }}
</h6>
<form action="#"> <form action="#">
<div class="mb-3"> <div class="font-option-group">
<label for="certificate_element_content" class="form-label">{{ get_phrase('Enter Text with variable data') }}</label> <div class="font-option-title">
<textarea name="certificate_element_content" placeholder="{{ get_phrase('Total Lesson') }}:{number_of_lesson}" id="certificate_element_content" rows="3" class="form-control"></textarea> <i class="fas fa-font"></i>{{ get_phrase('Text Content') }}
</div>
<div class="mb-3">
<label for="certificate_element_content" class="form-label">{{ get_phrase('Enter Text with variables') }}</label>
<textarea name="certificate_element_content"
placeholder="{{ get_phrase('Example: This certifies that {student_name} has completed {course_title}') }}"
id="certificate_element_content"
rows="3"
class="form-control"></textarea>
</div>
</div> </div>
<div class="mb-3">
<label for="font_family_auto" class="form-label">{{ get_phrase('Choice a font-family') }}</label><br> <div class="font-option-group">
<input type="radio" name="font_family" value="auto" id="font_family_auto" checked> <label for="font_family_auto">{{ get_phrase('Auto') }}</label><br> <div class="font-option-title">
<input type="radio" name="font_family" value="Pinyon Script" id="font_family_pinyon_script"> <label for="font_family_pinyon_script">{{ get_phrase('Pinyon Script') }}</label><br> <i class="fas fa-palette"></i>{{ get_phrase('Font Styling') }}
</div>
<div class="mb-3">
<label for="font_family" class="form-label">{{ get_phrase('Font Family') }}</label>
<div id="font-family-options">
<!-- Font options will be populated by JavaScript -->
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Font Weight') }}</label>
<input type="range"
class="form-range font-weight-slider"
id="font_weight"
min="100"
max="900"
step="100"
value="400">
<div class="d-flex justify-content-between">
<small>Thin</small>
<small>Normal</small>
<small>Bold</small>
<small>Black</small>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Font Style') }}</label>
<div class="font-style-options">
<button type="button" class="font-style-btn" data-style="normal">
<i class="fas fa-font"></i> Normal
</button>
<button type="button" class="font-style-btn" data-style="italic">
<i class="fas fa-italic"></i> Italic
</button>
<button type="button" class="font-style-btn" data-style="oblique">
<i class="fas fa-slash"></i> Oblique
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Text Decoration') }}</label>
<div class="text-decoration-options">
<button type="button" class="text-decoration-btn" data-decoration="none">
None
</button>
<button type="button" class="text-decoration-btn" data-decoration="underline">
<u>Underline</u>
</button>
<button type="button" class="text-decoration-btn" data-decoration="overline">
Overline
</button>
<button type="button" class="text-decoration-btn" data-decoration="line-through">
<s>Strike</s>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Text Color') }}</label>
<div class="color-picker-container">
<input type="color"
id="text_color"
value="#000000"
style="position: absolute; opacity: 0; width: 30px; height: 30px; cursor: pointer;">
<div class="color-preview" id="color_preview" style="background-color: #000000;"></div>
</div>
</div>
<div class="mb-3">
<label for="font_size" class="form-label">{{ get_phrase('Font Size') }}: <span id="font_size_value">16</span>px</label>
<input type="range"
class="form-range"
id="font_size"
min="8"
max="72"
step="1"
value="16">
</div>
</div> </div>
<div class="mb-3">
<label for="font_size" class="form-label">{{ get_phrase('Font Size') }}</label><br> <div class="font-option-group">
<input type="number" name="font_size" value="16" id="font_size" class="form-control" required> <div class="font-option-title">
<i class="fas fa-text-height"></i>{{ get_phrase('Text Alignment') }}
</div>
<div class="mb-3">
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('left')">
<i class="fas fa-align-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('center')">
<i class="fas fa-align-center"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('right')">
<i class="fas fa-align-right"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('justify')">
<i class="fas fa-align-justify"></i>
</button>
</div>
</div>
</div> </div>
<div class="mb-3">
<button type="button" class="btn ol-btn-light-primary w-100" onclick="addElemToCertificate()">{{ get_phrase('Add') }}</button> <div class="mb-4">
<button type="button" class="btn ol-btn-light-primary w-100" onclick="addElemToCertificate()">
<i class="fas fa-plus me-2"></i>{{ get_phrase('Add Element') }}
</button>
</div> </div>
<div class="mb-5">
<button type="button" class="btn ol-btn-primary w-100" onclick="saveTemplate()">{{ get_phrase('Save Template') }}</button> <div class="mb-4">
<button type="button" class="btn ol-btn-primary w-100" onclick="saveTemplate()">
<i class="fas fa-save me-2"></i>{{ get_phrase('Save Template') }}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="certificate_builder_content" class="builder dotted-background"> <div id="certificate_builder_content" class="builder dotted-background">
{{-- Common style for page builder start --}} {{-- Common style for page builder start --}}
<style> <style>
/* Import Google Fonts for certificate styling */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Pinyon+Script&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Italianno&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Italianno&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Pinyon+Script&display=swap%27'); @import url('https://fonts.googleapis.com/css2?family=Miss+Fajardose&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Miss+Fajardose&display=swap%27'); @import url('https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@100;300;400;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+Pro:wght@200;300;400;600;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Tangerine:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Alex+Brush&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Sacramento&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Parisienne&display=swap');
</style> </style>
{{-- Common style for page builder END --}} {{-- Common style for page builder END --}}
@if (get_settings('certificate_builder_content')) @if (get_settings('certificate_builder_content'))
@php @php
$htmlContent = get_settings('certificate_builder_content'); $htmlContent = get_settings('certificate_builder_content');
// Use regex to update the src attribute of the <img> tag with the class 'certificate-template'.
$newSrc = get_image(get_settings('certificate_template')); $newSrc = get_image(get_settings('certificate_template'));
$certificate_builder_content = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $htmlContent); $certificate_builder_content = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $htmlContent);
@endphp @endphp
{!! $certificate_builder_content !!} {!! $certificate_builder_content !!}
@ -220,9 +556,140 @@
</div> </div>
@endif @endif
</div> </div>
<script> <script>
"use strict"; "use strict";
// Available fonts with categories
const fontOptions = [
{ name: 'Auto', value: 'auto', category: 'default' },
{ name: 'Inter', value: 'Inter, sans-serif', category: 'modern' },
{ name: 'Pinyon Script', value: 'Pinyon Script, cursive', category: 'elegant' },
{ name: 'Italianno', value: 'Italianno, cursive', category: 'script' },
{ name: 'Miss Fajardose', value: 'Miss Fajardose, cursive', category: 'script' },
{ name: 'Great Vibes', value: 'Great Vibes, cursive', category: 'script' },
{ name: 'Dancing Script', value: 'Dancing Script, cursive', category: 'script' },
{ name: 'Playfair Display', value: 'Playfair Display, serif', category: 'serif' },
{ name: 'Montserrat', value: 'Montserrat, sans-serif', category: 'modern' },
{ name: 'Roboto', value: 'Roboto, sans-serif', category: 'modern' },
{ name: 'Lato', value: 'Lato, sans-serif', category: 'modern' },
{ name: 'Open Sans', value: 'Open Sans, sans-serif', category: 'modern' },
{ name: 'Merriweather', value: 'Merriweather, serif', category: 'serif' },
{ name: 'Cinzel', value: 'Cinzel, serif', category: 'decorative' },
{ name: 'Raleway', value: 'Raleway, sans-serif', category: 'modern' },
{ name: 'Source Serif Pro', value: 'Source Serif Pro, serif', category: 'serif' },
{ name: 'Cormorant Garamond', value: 'Cormorant Garamond, serif', category: 'serif' },
{ name: 'Tangerine', value: 'Tangerine, cursive', category: 'script' },
{ name: 'Alex Brush', value: 'Alex Brush, cursive', category: 'script' },
{ name: 'Sacramento', value: 'Sacramento, cursive', category: 'script' },
{ name: 'Parisienne', value: 'Parisienne, cursive', category: 'script' }
];
// Current styling options
let currentFontStyle = 'normal';
let currentTextDecoration = 'none';
let currentTextAlign = 'left';
let currentTextColor = '#000000';
$(document).ready(function() {
initialize();
populateFontOptions();
setupEventListeners();
});
function populateFontOptions() {
const container = $('#font-family-options');
container.empty();
// Group fonts by category
const fontsByCategory = {};
fontOptions.forEach(font => {
if (!fontsByCategory[font.category]) {
fontsByCategory[font.category] = [];
}
fontsByCategory[font.category].push(font);
});
// Create options for each category
Object.keys(fontsByCategory).forEach(category => {
const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
const categoryDiv = $(`<div class="mb-3"><small class="text-muted">${categoryTitle} Fonts</small></div>`);
fontsByCategory[category].forEach(font => {
const isActive = font.value === 'auto' ? 'active' : '';
const option = $(`
<div class="font-family-option ${isActive}" data-value="${font.value}">
<input type="radio" name="font_family" value="${font.value}"
id="font_family_${font.value.replace(/\s+/g, '_')}"
${font.value === 'auto' ? 'checked' : ''}>
<label for="font_family_${font.value.replace(/\s+/g, '_')}" class="ms-2 flex-grow-1">${font.name}</label>
<span class="font-family-preview" style="font-family: ${font.value}">Aa</span>
</div>
`);
categoryDiv.append(option);
});
container.append(categoryDiv);
});
// Add click event to font options
$('.font-family-option').click(function() {
$('.font-family-option').removeClass('active');
$(this).addClass('active');
$(this).find('input[type="radio"]').prop('checked', true);
});
}
function setupEventListeners() {
// Font size display
$('#font_size').on('input', function() {
$('#font_size_value').text($(this).val());
});
// Font style buttons
$('.font-style-btn').click(function() {
$('.font-style-btn').removeClass('active');
$(this).addClass('active');
currentFontStyle = $(this).data('style');
});
// Text decoration buttons
$('.text-decoration-btn').click(function() {
$('.text-decoration-btn').removeClass('active');
$(this).addClass('active');
currentTextDecoration = $(this).data('decoration');
});
// Color picker
$('#text_color').on('input', function() {
$('#color_preview').css('background-color', $(this).val());
currentTextColor = $(this).val();
});
// Initialize first button as active
$('.font-style-btn[data-style="normal"]').addClass('active');
$('.text-decoration-btn[data-decoration="none"]').addClass('active');
}
function addVariableToText(variable) {
const textarea = $('#certificate_element_content');
const currentValue = textarea.val();
const cursorPos = textarea[0].selectionStart;
const newValue = currentValue.substring(0, cursorPos) +
variable +
currentValue.substring(cursorPos);
textarea.val(newValue);
textarea.focus();
}
function setTextAlignment(alignment) {
currentTextAlign = alignment;
$('.btn-outline-secondary').removeClass('active');
$(`button[onclick="setTextAlignment('${alignment}')"]`).addClass('active');
}
function saveTemplate() { function saveTemplate() {
var certificate_builder_content = $('#certificate_builder_content').html(); var certificate_builder_content = $('#certificate_builder_content').html();
$.ajax({ $.ajax({
@ -237,50 +704,93 @@
success: function(response) { success: function(response) {
$(location).attr('href', response); $(location).attr('href', response);
console.log(response) console.log(response)
},
error: function(xhr) {
console.error('Error saving template:', xhr);
alert('Error saving template. Please try again.');
} }
}); });
} }
function addElemToCertificate() { function addElemToCertificate() {
var font_family = $("input[type='radio'][name='font_family']:checked").val(); var font_family = $("input[type='radio'][name='font_family']:checked").val();
console.log(font_family);
var font_size = $("#font_size").val(); var font_size = $("#font_size").val();
var font_weight = $("#font_weight").val();
var certificate_element_content = $('#certificate_element_content').val(); var certificate_element_content = $('#certificate_element_content').val();
var certificateElem = `<div class="draggable resizeable-canvas" style="padding: 5px !important; position: absolute; font-size: ${font_size}px; top: 10px; left: 10px; width: min-content; font-family: '${font_family}';">
${certificate_element_content}
<i class="remove-item fi-rr-cross-circle cursor-pointer" onclick="$(this).parent().remove()">
</div>`;
if (certificate_element_content != '') { // Generate unique ID for the element
var elementId = 'elem_' + Date.now() + Math.floor(Math.random() * 1000);
var certificateElem = `
<div id="${elementId}"
class="draggable resizeable-canvas"
style="
padding: 10px !important;
position: absolute;
font-size: ${font_size}px;
font-weight: ${font_weight};
font-style: ${currentFontStyle};
text-decoration: ${currentTextDecoration};
text-align: ${currentTextAlign};
color: ${currentTextColor};
top: 50px;
left: 50px;
min-width: 100px;
min-height: 40px;
font-family: ${font_family};
border-radius: 8px;
z-index: 5;
">
${certificate_element_content}
<div class="remove-item" onclick="$(this).parent().remove(); positionTracking(this.parentNode)">
<i class="fas fa-times"></i>
</div>
</div>`;
if (certificate_element_content.trim() !== '') {
$('#certificate-layout-module').append(certificateElem); $('#certificate-layout-module').append(certificateElem);
// Reset form
$('#certificate_element_content').val(''); $('#certificate_element_content').val('');
$("#font_size").val(16); $("#font_size").val(16);
$("#font_family_auto").attr('checked', 'checked'); $("#font_size_value").text('16');
$("#font_weight").val(400);
currentTextColor = '#000000';
$('#color_preview').css('background-color', '#000000');
$('#text_color').val('#000000');
// Reset to default font
$('.font-family-option').removeClass('active');
$('.font-family-option[data-value="auto"]').addClass('active').find('input').prop('checked', true);
initialize(); initialize();
} else {
alert('Please enter some text content.');
} }
} }
$(document).ready(function() {
initialize();
});
function initialize() { function initialize() {
$(".draggable").draggable(); $(".draggable").draggable({
containment: "#certificate-layout-module",
cursor: "move",
scroll: false
});
$(".resizeable-canvas").resizable({ $(".resizeable-canvas").resizable({
resize: function(event, ui) { resize: function(event, ui) {
// When resizing starts, temporarily disable dragging
$(".draggable").draggable("disable"); $(".draggable").draggable("disable");
positionTracking(this); positionTracking(this);
}, },
stop: function(event, ui) { stop: function(event, ui) {
$(".draggable").draggable("enable"); $(".draggable").draggable("enable");
positionTracking(this); positionTracking(this);
} },
handles: "all",
minHeight: 30,
minWidth: 50
}); });
$(".draggable").on("dragstop", function(e, pos) {
$(".draggable").on("dragend", function(e, pos) {}).on("dragstart", function(e, pos) {}).on("dragstop", function(e, pos) {
positionTracking(this); positionTracking(this);
}); });
} }
@ -302,15 +812,20 @@
layoutCanvasBottom > itemBottom layoutCanvasBottom > itemBottom
) { ) {
$(e).removeClass('hidden-position'); $(e).removeClass('hidden-position');
$(e).css('border-color', 'rgba(21, 181, 126, 0.5)');
} else { } else {
$(e).addClass('hidden-position'); $(e).addClass('hidden-position');
$(e).css('border-color', '#ff6b6b');
} }
} else { } else {
var draggableItems = document.getElementsByClassName("draggable"); var draggableItems = document.getElementsByClassName("draggable");
for (var i = 0; i < draggableItems.length; i++) { for (var i = 0; i < draggableItems.length; i++) {
var itemOffset = $(draggableItems.item(i)).offset(); var item = $(draggableItems.item(i));
var itemRight = $(draggableItems.item(i)).width() + itemOffset.left; if (item.attr('id') == 'certificate-layout-module') continue;
var itemBottom = $(draggableItems.item(i)).height() + itemOffset.top;
var itemOffset = item.offset();
var itemRight = item.width() + itemOffset.left;
var itemBottom = item.height() + itemOffset.top;
if ( if (
layoutCanvasOffset.left < itemOffset.left && layoutCanvasOffset.left < itemOffset.left &&
@ -318,9 +833,11 @@
layoutCanvasRight > itemRight && layoutCanvasRight > itemRight &&
layoutCanvasBottom > itemBottom layoutCanvasBottom > itemBottom
) { ) {
$(draggableItems.item(i)).removeClass('hidden-position'); item.removeClass('hidden-position');
item.css('border-color', 'rgba(21, 181, 126, 0.5)');
} else { } else {
$(draggableItems.item(i)).addClass('hidden-position'); item.addClass('hidden-position');
item.css('border-color', '#ff6b6b');
} }
} }
} }
@ -328,5 +845,4 @@
</script> </script>
</body> </body>
</html> </html>

View File

@ -9,7 +9,7 @@
<link rel="shortcut icon" href="{{ asset(get_frontend_settings('favicon')) }}" /> <link rel="shortcut icon" href="{{ asset(get_frontend_settings('favicon')) }}" />
<script src="{{ asset('assets/frontend/default/js/jquery-3.7.1.min.js') }}"></script> <script src="{{ asset('assets/frontend/default/js/jquery-3.7.1.min.js') }}"></script>
<script src="{{ asset('assets/global/html2canvas/html2canvas.min.js') }}"></script> <script src="{{ asset('assets/global/html2canvas/html2canvas.min.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
</head> </head>
<body> <body>
@ -26,31 +26,83 @@
display: none !important; display: none !important;
} }
.certificate-layout-module { .certificate-layout-module {
left: unset !important; left: unset !important;
top: unset !important; top: unset !important;
margin-left: auto !important; margin-left: auto !important;
margin-right: auto !important; margin-right: auto !important;
} }
svg { svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.download-wrapper {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 15px;
z-index: 100;
}
.download-btn {
padding: 12px 24px;
border-radius: 8px;
color: #ffffff;
background: linear-gradient(135deg, #3d9bff 0%, #0066cc 100%);
border: none;
font-weight: 600;
font-size: 16px;
text-decoration: none;
box-shadow: 0 4px 12px rgba(61, 155, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
display: inline-block;
white-space: nowrap;
text-align: center;
min-width: 160px;
}
.download-btn.secondary {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
}
.download-btn.secondary:hover {
background: linear-gradient(135deg, #5a6268 0%, #3d4349 100%);
box-shadow: 0 6px 16px rgba(108, 117, 125, 0.4);
}
.download-btn:hover {
background: linear-gradient(135deg, #2d8bef 0%, #0055b3 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(61, 155, 255, 0.4);
}
.download-btn:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(61, 155, 255, 0.3);
}
.download-btn:focus {
outline: 2px solid rgba(61, 155, 255, 0.5);
outline-offset: 2px;
}
/* Responsive untuk mobile */
@media (max-width: 768px) {
.download-btn { .download-btn {
position: fixed; bottom: 16px;
bottom: 10px; right: 16px;
right: 10px; padding: 10px 20px;
padding: 20px 32px; font-size: 14px;
border-radius: 5px;
color: #3d9bff;
background-color: #d3e8ff;
z-index: 100;
} }
}
.absolute-view{ .absolute-view{
background-color: #fff; background-color: #e6e6e6;
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -75,12 +127,37 @@
{{-- Downloadable canvas --}} {{-- Downloadable canvas --}}
<div class="captureCertificate" id="captureCertificate"> <div class="captureCertificate" id="captureCertificate">
@php @php
$bulan_indonesia = [
1 => 'Januari',
2 => 'Februari',
3 => 'Maret',
4 => 'April',
5 => 'Mei',
6 => 'Juni',
7 => 'Juli',
8 => 'Agustus',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Desember'
];
// Jika date_formatter() mengembalikan tanggal string
$completion_date = $certificate->created_at; // Ambil timestamp asli
// Konversi ke format Indonesia
$timestamp = strtotime($completion_date); // atau $completion_date jika sudah timestamp
$hari = date('j', $timestamp); // tanggal tanpa nol
$bulan_angka = (int)date('n', $timestamp); // bulan angka
$tahun = date('Y', $timestamp);
$course_completion_date = $hari . ' ' . $bulan_indonesia[$bulan_angka] . ' ' . $tahun;
$course_duration = $certificate->course->total_duration(); $course_duration = $certificate->course->total_duration();
$student_name = $certificate->user->name; $student_name = $certificate->user->name;
$course_title = $certificate->course->title; $course_title = $certificate->course->title;
$number_of_lesson = $certificate->course->lessons->count(); $number_of_lesson = $certificate->course->lessons->count();
$qr_code = $qrcode; $qr_code = $qrcode;
$course_completion_date = date_formatter($certificate->created_at); $certificate_id = $certificate->identifier;
$certificate_download_date = date('d M Y'); $certificate_download_date = date('d M Y');
$course_level = ucfirst($certificate->course->level); $course_level = ucfirst($certificate->course->level);
$course_language = ucfirst($certificate->course->language); $course_language = ucfirst($certificate->course->language);
@ -98,6 +175,7 @@
$certificate_builder_content = str_replace('{number_of_lesson}', $number_of_lesson, $certificate_builder_content); $certificate_builder_content = str_replace('{number_of_lesson}', $number_of_lesson, $certificate_builder_content);
$certificate_builder_content = str_replace('{qr_code}', $qr_code, $certificate_builder_content); $certificate_builder_content = str_replace('{qr_code}', $qr_code, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_completion_date}', $course_completion_date, $certificate_builder_content); $certificate_builder_content = str_replace('{course_completion_date}', $course_completion_date, $certificate_builder_content);
$certificate_builder_content = str_replace('{certificate_id}', $certificate_id, $certificate_builder_content);
$certificate_builder_content = str_replace('{certificate_download_date}', $certificate_download_date, $certificate_builder_content); $certificate_builder_content = str_replace('{certificate_download_date}', $certificate_download_date, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_level}', $course_level, $certificate_builder_content); $certificate_builder_content = str_replace('{course_level}', $course_level, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_language}', $course_language, $certificate_builder_content); $certificate_builder_content = str_replace('{course_language}', $course_language, $certificate_builder_content);
@ -121,7 +199,19 @@
{{-- Preview certificate end--}} {{-- Preview certificate end--}}
<a class="download-btn" href="#" onclick="setTimeout(() => renderCanvasToImage(), 500);">{{ get_phrase('Download') }}</a> <div class="download-wrapper">
<a class="download-btn" href="#"
onclick="setTimeout(() => renderCanvasToImage(), 500); return false;">
{{ get_phrase('Download Image') }}
</a>
<a class="download-btn secondary" href="#"
onclick="renderCanvasToPDF(); return false;">
{{ get_phrase('Download PDF') }}
</a>
</div>
<script> <script>
"use strict"; "use strict";
@ -176,6 +266,57 @@
// Remove the link from the body // Remove the link from the body
document.body.removeChild(link); document.body.removeChild(link);
} }
$(function() {
var certificate_builder_view_width = $('.certificate_builder_view').width();
var certificate_layout_module = $('.certificate_builder_view .certificate-layout-module').width();
var zoomScaleValue = ((certificate_builder_view_width / certificate_layout_module) * 100) - 8;
$('.certificate_builder_view .certificate-layout-module').css('zoom', zoomScaleValue + '%');
});
function renderCanvasToPDF() {
const target = document.querySelector("#captureCertificate > div");
html2canvas(target, {
allowTaint: true,
useCORS: true,
scale: 2
}).then(canvas => {
const imgData = canvas.toDataURL("image/png");
// A4 Landscape
const pdf = new window.jspdf.jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
});
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
// Hitung rasio agar tidak terpotong
const imgWidth = pageWidth;
const imgHeight = canvas.height * imgWidth / canvas.width;
const positionY = (pageHeight - imgHeight) / 2;
pdf.addImage(
imgData,
'PNG',
0,
positionY,
imgWidth,
imgHeight,
undefined,
'FAST'
);
pdf.save("certificate.pdf");
});
}
</script> </script>
</body> </body>