init commit
This commit is contained in:
88
public/css/custom.css
Normal file
88
public/css/custom.css
Normal file
@@ -0,0 +1,88 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* Basic reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Additional styles for demo */
|
||||
.hero-section {
|
||||
padding: 6rem 0 4rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 4rem 0 3rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
315
public/css/dark-theme.css
Normal file
315
public/css/dark-theme.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/* Dark Theme Support for SmartSolTech */
|
||||
|
||||
/* Base Dark Theme */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Navigation Dark Theme */
|
||||
.dark nav {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .nav-link {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .nav-link:hover {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark .nav-link.active {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Sections Dark Theme */
|
||||
.dark section {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .bg-white {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-50 {
|
||||
background-color: #111827 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-100 {
|
||||
background-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-900 {
|
||||
background-color: #030712 !important;
|
||||
}
|
||||
|
||||
/* Text Colors Dark Theme */
|
||||
.dark .text-gray-900 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-800 {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-700 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-600 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-500 {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-400 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-300 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Cards Dark Theme */
|
||||
.dark .card-hover {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .card-hover:hover {
|
||||
background-color: #374151;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Forms Dark Theme */
|
||||
.dark .form-input {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark input,
|
||||
.dark textarea,
|
||||
.dark select {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark input:focus,
|
||||
.dark textarea:focus,
|
||||
.dark select:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Borders Dark Theme */
|
||||
.dark .border-gray-300 {
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.dark .border-gray-200 {
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .border-t {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Contact Form Dark Theme */
|
||||
.dark .contact-form {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Service Cards Dark Theme */
|
||||
.dark .service-card {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Team Cards Dark Theme */
|
||||
.dark .team-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Portfolio Items Dark Theme */
|
||||
.dark .portfolio-item {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Hero Section Dark Theme */
|
||||
.dark .hero-section {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
}
|
||||
|
||||
/* Shadows Dark Theme */
|
||||
.dark .shadow-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-xl {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-2xl {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dropdown Dark Theme */
|
||||
.dark .dropdown-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a:hover {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Icons Dark Theme */
|
||||
.dark .text-blue-600 {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
.dark .text-purple-600 {
|
||||
color: #a78bfa !important;
|
||||
}
|
||||
|
||||
.dark .text-green-600 {
|
||||
color: #34d399 !important;
|
||||
}
|
||||
|
||||
.dark .text-yellow-600 {
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
/* Buttons Dark Theme */
|
||||
.dark .btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.dark .btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8, #1e40af);
|
||||
}
|
||||
|
||||
/* Footer Dark Theme */
|
||||
.dark footer {
|
||||
background-color: #030712;
|
||||
border-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .footer-gradient {
|
||||
background: linear-gradient(135deg, #030712 0%, #111827 100%);
|
||||
}
|
||||
|
||||
/* Scrollbar Dark Theme */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
/* Technology Stack Dark Theme */
|
||||
.dark .tech-stack {
|
||||
background-color: #030712;
|
||||
}
|
||||
|
||||
.dark .tech-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* CTA Section Dark Theme */
|
||||
.dark .cta-section {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
|
||||
}
|
||||
|
||||
/* Testimonials Dark Theme */
|
||||
.dark .testimonial-card {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Alert Messages Dark Theme */
|
||||
.dark .alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Mobile Menu Dark Theme */
|
||||
.dark #mobile-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Language Dropdown Dark Theme */
|
||||
.dark #mobile-language-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Smooth Theme Transition */
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Print styles for dark theme */
|
||||
@media print {
|
||||
.dark * {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.dark .border {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dark * {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
281
public/css/fixes.css
Normal file
281
public/css/fixes.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/* SmartSolTech - Design Fixes & Enhancements */
|
||||
|
||||
/* Glass effect improvements */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
/* Fallback for browsers that don't support backdrop-filter */
|
||||
}
|
||||
|
||||
/* Support backdrop-filter for modern browsers */
|
||||
@supports (backdrop-filter: blur(10px)) {
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero section improvements */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Background blob animations */
|
||||
@keyframes blob {
|
||||
0% { transform: translate(0px, 0px) scale(1); }
|
||||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||||
100% { transform: translate(0px, 0px) scale(1); }
|
||||
}
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
/* Enhanced card hover effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Portfolio item enhancements */
|
||||
.portfolio-item {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Button improvements */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1D4ED8, #1E40AF);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Navigation improvements */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Form improvements */
|
||||
.form-input {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #E5E7EB;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3B82F6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Contact form styling */
|
||||
.contact-form {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95));
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* CTA section improvements */
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Service cards */
|
||||
.service-card {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Team member cards */
|
||||
.team-card {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
/* Technology icons */
|
||||
.tech-icon {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tech-icon:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3B82F6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
a:focus {
|
||||
outline: 2px solid #3B82F6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support (if needed) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.auto-dark {
|
||||
color: #f9fafb;
|
||||
background-color: #111827;
|
||||
}
|
||||
}
|
||||
552
public/css/main.css
Normal file
552
public/css/main.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calculator-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.service-option:hover,
|
||||
.complexity-option:hover,
|
||||
.timeline-option:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected,
|
||||
.complexity-option.selected,
|
||||
.timeline-option.selected {
|
||||
border-color: #3B82F6 !important;
|
||||
background-color: #EBF8FF !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected::after,
|
||||
.complexity-option.selected::after,
|
||||
.timeline-option.selected::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Price Display Animation */
|
||||
#final-price {
|
||||
animation: priceReveal 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes priceReveal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculator Progress Bar */
|
||||
.calculator-progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculator-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-2 {
|
||||
width: 66.66%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Calculator Mobile Improvements */
|
||||
@media (max-width: 768px) {
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#final-price {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
384
public/js/calculator.js
Normal file
384
public/js/calculator.js
Normal file
@@ -0,0 +1,384 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services';
|
||||
const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity';
|
||||
const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline';
|
||||
|
||||
summaryHTML += `<div class="mb-4"><strong>${servicesLabel}:</strong></div>`;
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>${complexityLabel}:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>${timelineLabel}:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.services) {
|
||||
return window.calculatorTranslations.services[service] || service;
|
||||
}
|
||||
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
|
||||
return window.calculatorTranslations.complexity[complexity] || complexity;
|
||||
}
|
||||
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
|
||||
return window.calculatorTranslations.timeline[timeline] || timeline;
|
||||
}
|
||||
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
544
public/js/main.js
Normal file
544
public/js/main.js
Normal file
@@ -0,0 +1,544 @@
|
||||
// Main JavaScript for SmartSolTech Website
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize AOS (Animate On Scroll)
|
||||
if (typeof AOS !== 'undefined') {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
easing: 'ease-in-out',
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile Navigation Toggle
|
||||
const mobileMenuButton = document.querySelector('.mobile-menu-button');
|
||||
const mobileMenu = document.querySelector('.mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('show');
|
||||
const isOpen = mobileMenu.classList.contains('show');
|
||||
|
||||
// Toggle button icon
|
||||
const icon = mobileMenuButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.innerHTML = isOpen
|
||||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />'
|
||||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />';
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
mobileMenuButton.setAttribute('aria-expanded', isOpen);
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) {
|
||||
mobileMenu.classList.remove('show');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Navbar Scroll Effect
|
||||
const navbar = document.querySelector('nav');
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (navbar) {
|
||||
if (currentScrollY > 100) {
|
||||
navbar.classList.add('navbar-scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('navbar-scrolled');
|
||||
}
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 200) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
});
|
||||
|
||||
// Smooth Scrolling for Anchor Links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0);
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Contact Form Handler
|
||||
const quickContactForm = document.getElementById('quick-contact-form');
|
||||
if (quickContactForm) {
|
||||
quickContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
const mainContactForm = document.getElementById('contact-form');
|
||||
if (mainContactForm) {
|
||||
mainContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
async function handleContactSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.classList.add('btn-loading');
|
||||
submitButton.textContent = '전송 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const response = await fetch('/api/contact/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success');
|
||||
form.reset();
|
||||
} else {
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('btn-loading');
|
||||
submitButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification System
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<span class="notification-message">${message}</span>
|
||||
<button class="notification-close" aria-label="Close notification">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Styles
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
${type === 'success' ? 'background: #10b981;' : ''}
|
||||
${type === 'error' ? 'background: #ef4444;' : ''}
|
||||
${type === 'info' ? 'background: #3b82f6;' : ''}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// Close button handler
|
||||
const closeButton = notification.querySelector('.notification-close');
|
||||
closeButton.addEventListener('click', () => {
|
||||
closeNotification(notification);
|
||||
});
|
||||
|
||||
// Auto close after 5 seconds
|
||||
setTimeout(() => {
|
||||
closeNotification(notification);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function closeNotification(notification) {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Portfolio Filter (if on portfolio page)
|
||||
const portfolioFilters = document.querySelectorAll('.portfolio-filter');
|
||||
const portfolioItems = document.querySelectorAll('.portfolio-item');
|
||||
|
||||
portfolioFilters.forEach(filter => {
|
||||
filter.addEventListener('click', function() {
|
||||
const category = this.dataset.category;
|
||||
|
||||
// Update active filter
|
||||
portfolioFilters.forEach(f => f.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Filter items
|
||||
portfolioItems.forEach(item => {
|
||||
if (category === 'all' || item.dataset.category === category) {
|
||||
item.style.display = 'block';
|
||||
item.style.animation = 'fadeIn 0.5s ease';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Image Lazy Loading
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
|
||||
// Service Worker Registration for PWA
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered successfully:', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance Monitoring
|
||||
if ('performance' in window) {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0];
|
||||
const loadTime = perfData.loadEventEnd - perfData.loadEventStart;
|
||||
|
||||
if (loadTime > 3000) {
|
||||
console.warn('Page load time is slow:', loadTime + 'ms');
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Cookie Consent (if needed)
|
||||
function initCookieConsent() {
|
||||
const consent = localStorage.getItem('cookieConsent');
|
||||
if (!consent) {
|
||||
showCookieConsent();
|
||||
}
|
||||
}
|
||||
|
||||
function showCookieConsent() {
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'cookie-consent';
|
||||
banner.innerHTML = `
|
||||
<div class="cookie-content">
|
||||
<p>이 웹사이트는 더 나은 서비스 제공을 위해 쿠키를 사용합니다.</p>
|
||||
<div class="cookie-buttons">
|
||||
<button id="accept-cookies" class="btn btn-primary">동의</button>
|
||||
<button id="decline-cookies" class="btn btn-secondary">거부</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
banner.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
z-index: 9999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
setTimeout(() => {
|
||||
banner.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
|
||||
document.getElementById('accept-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'accepted');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
|
||||
document.getElementById('decline-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'declined');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize cookie consent
|
||||
// initCookieConsent();
|
||||
|
||||
// Parallax Effect
|
||||
const parallaxElements = document.querySelectorAll('.parallax');
|
||||
|
||||
function updateParallax() {
|
||||
const scrollY = window.pageYOffset;
|
||||
|
||||
parallaxElements.forEach(element => {
|
||||
const speed = element.dataset.speed || 0.5;
|
||||
const yPos = -(scrollY * speed);
|
||||
element.style.transform = `translateY(${yPos}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
if (parallaxElements.length > 0) {
|
||||
window.addEventListener('scroll', updateParallax);
|
||||
}
|
||||
|
||||
// Counter Animation
|
||||
const counters = document.querySelectorAll('.counter');
|
||||
const counterObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounter(entry.target);
|
||||
counterObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
counters.forEach(counter => counterObserver.observe(counter));
|
||||
|
||||
function animateCounter(element) {
|
||||
const target = parseInt(element.dataset.count);
|
||||
const duration = 2000;
|
||||
const start = performance.now();
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const current = Math.floor(progress * target);
|
||||
element.textContent = current.toLocaleString();
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
|
||||
// Typing Effect for Hero Text
|
||||
const typingElements = document.querySelectorAll('.typing-effect');
|
||||
|
||||
typingElements.forEach(element => {
|
||||
const text = element.textContent;
|
||||
element.textContent = '';
|
||||
element.style.borderRight = '2px solid';
|
||||
|
||||
let i = 0;
|
||||
const timer = setInterval(() => {
|
||||
element.textContent += text[i];
|
||||
i++;
|
||||
|
||||
if (i >= text.length) {
|
||||
clearInterval(timer);
|
||||
element.style.borderRight = 'none';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Handle form validation
|
||||
const forms = document.querySelectorAll('form[data-validate]');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(this)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
const inputs = form.querySelectorAll('input, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('blur', () => validateField(input));
|
||||
input.addEventListener('input', () => clearFieldError(input));
|
||||
});
|
||||
});
|
||||
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const inputs = form.querySelectorAll('input[required], textarea[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (!validateField(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateField(field) {
|
||||
const value = field.value.trim();
|
||||
const type = field.type;
|
||||
let isValid = true;
|
||||
let message = '';
|
||||
|
||||
// Required field check
|
||||
if (field.hasAttribute('required') && !value) {
|
||||
isValid = false;
|
||||
message = '이 필드는 필수입니다.';
|
||||
}
|
||||
|
||||
// Email validation
|
||||
if (type === 'email' && value) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 이메일 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Phone validation
|
||||
if (type === 'tel' && value) {
|
||||
const phoneRegex = /^[0-9-+\s()]+$/;
|
||||
if (!phoneRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 전화번호 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide error
|
||||
if (!isValid) {
|
||||
showFieldError(field, message);
|
||||
} else {
|
||||
clearFieldError(field);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function showFieldError(field, message) {
|
||||
clearFieldError(field);
|
||||
|
||||
field.classList.add('error');
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'field-error';
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;';
|
||||
|
||||
field.parentNode.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
function clearFieldError(field) {
|
||||
field.classList.remove('error');
|
||||
const errorDiv = field.parentNode.querySelector('.field-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Utility Functions
|
||||
const utils = {
|
||||
// Debounce function
|
||||
debounce: function(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// Throttle function
|
||||
throttle: function(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Format currency
|
||||
formatCurrency: function(amount, currency = 'KRW') {
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate: function(date, options = {}) {
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
};
|
||||
return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date));
|
||||
}
|
||||
};
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(e) {
|
||||
console.error('Global error:', e.error);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = utils;
|
||||
}
|
||||
161
public/manifest.json
Normal file
161
public/manifest.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"name": "SmartSolTech - Technology Solutions",
|
||||
"short_name": "SmartSolTech",
|
||||
"description": "Professional web development, mobile apps, and digital solutions in Korea",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#3b82f6",
|
||||
"background_color": "#ffffff",
|
||||
"lang": "ko",
|
||||
"scope": "/",
|
||||
"categories": ["business", "productivity", "technology"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot-desktop.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "SmartSolTech Desktop View"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshot-mobile.png",
|
||||
"sizes": "375x812",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "SmartSolTech Mobile View"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Portfolio",
|
||||
"short_name": "Portfolio",
|
||||
"description": "View our latest projects",
|
||||
"url": "/portfolio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-portfolio.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Calculator",
|
||||
"short_name": "Calculator",
|
||||
"description": "Calculate project costs",
|
||||
"url": "/calculator",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-calculator.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"short_name": "Contact",
|
||||
"description": "Get in touch with us",
|
||||
"url": "/contact",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-contact.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [
|
||||
{
|
||||
"platform": "webapp",
|
||||
"url": "https://smartsoltech.kr/manifest.json"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false,
|
||||
"edge_side_panel": {
|
||||
"preferred_width": 400
|
||||
},
|
||||
"protocol_handlers": [
|
||||
{
|
||||
"protocol": "mailto",
|
||||
"url": "/contact?email=%s"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/open-file",
|
||||
"accept": {
|
||||
"image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
||||
"application/pdf": [".pdf"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "images",
|
||||
"accept": ["image/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
396
public/sw.js
Normal file
396
public/sw.js
Normal file
@@ -0,0 +1,396 @@
|
||||
// Service Worker for SmartSolTech PWA
|
||||
const CACHE_NAME = 'smartsoltech-v1.0.0';
|
||||
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0';
|
||||
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0';
|
||||
|
||||
// Files to cache immediately
|
||||
const STATIC_FILES = [
|
||||
'/',
|
||||
'/css/main.css',
|
||||
'/js/main.js',
|
||||
'/images/logo.png',
|
||||
'/images/icon-192x192.png',
|
||||
'/images/icon-512x512.png',
|
||||
'/manifest.json',
|
||||
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css',
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js'
|
||||
];
|
||||
|
||||
// Routes to cache dynamically
|
||||
const DYNAMIC_ROUTES = [
|
||||
'/about',
|
||||
'/services',
|
||||
'/portfolio',
|
||||
'/calculator',
|
||||
'/contact'
|
||||
];
|
||||
|
||||
// API endpoints to cache
|
||||
const API_CACHE_PATTERNS = [
|
||||
/^\/api\/portfolio/,
|
||||
/^\/api\/services/,
|
||||
/^\/api\/calculator\/services/
|
||||
];
|
||||
|
||||
// Install event - cache static files
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Service Worker: Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Service Worker: Caching static files');
|
||||
return cache.addAll(STATIC_FILES);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Static files cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker: Error caching static files', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Service Worker: Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== STATIC_CACHE_NAME &&
|
||||
cacheName !== DYNAMIC_CACHE_NAME) {
|
||||
console.log('Service Worker: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve cached files or fetch from network
|
||||
self.addEventListener('fetch', event => {
|
||||
const request = event.request;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip Chrome extension requests
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different types of requests
|
||||
if (isStaticFile(request.url)) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
} else if (isAPIRequest(request.url)) {
|
||||
event.respondWith(networkFirst(request));
|
||||
} else if (isDynamicRoute(request.url)) {
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
} else {
|
||||
event.respondWith(networkFirst(request));
|
||||
}
|
||||
});
|
||||
|
||||
// Cache strategies
|
||||
async function cacheFirst(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('Cache first strategy failed:', error);
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('Network first: Falling back to cache for', request.url);
|
||||
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline.html') || new Response('Offline', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Network Error', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
async function staleWhileRevalidate(request) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
const cachedResponse = await cache.match(request);
|
||||
|
||||
const fetchPromise = fetch(request).then(networkResponse => {
|
||||
if (networkResponse.ok) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
});
|
||||
|
||||
return cachedResponse || fetchPromise;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function isStaticFile(url) {
|
||||
return url.includes('/css/') ||
|
||||
url.includes('/js/') ||
|
||||
url.includes('/images/') ||
|
||||
url.includes('/fonts/') ||
|
||||
url.includes('googleapis.com') ||
|
||||
url.includes('cdnjs.cloudflare.com');
|
||||
}
|
||||
|
||||
function isAPIRequest(url) {
|
||||
return url.includes('/api/') ||
|
||||
API_CACHE_PATTERNS.some(pattern => pattern.test(url));
|
||||
}
|
||||
|
||||
function isDynamicRoute(url) {
|
||||
const pathname = new URL(url).pathname;
|
||||
return DYNAMIC_ROUTES.includes(pathname) ||
|
||||
pathname.startsWith('/portfolio/') ||
|
||||
pathname.startsWith('/services/');
|
||||
}
|
||||
|
||||
// Background sync for form submissions
|
||||
self.addEventListener('sync', event => {
|
||||
console.log('Service Worker: Background sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'contact-form-sync') {
|
||||
event.waitUntil(syncContactForms());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContactForms() {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
const contactRequests = requests.filter(request =>
|
||||
request.url.includes('/api/contact/submit')
|
||||
);
|
||||
|
||||
for (const request of contactRequests) {
|
||||
try {
|
||||
await fetch(request);
|
||||
await cache.delete(request);
|
||||
console.log('Contact form synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to sync contact form:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Background sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Push notification handling
|
||||
self.addEventListener('push', event => {
|
||||
console.log('Service Worker: Push received', event);
|
||||
|
||||
let data = {};
|
||||
if (event.data) {
|
||||
data = event.data.json();
|
||||
}
|
||||
|
||||
const title = data.title || 'SmartSolTech';
|
||||
const options = {
|
||||
body: data.body || 'You have a new notification',
|
||||
icon: '/images/icon-192x192.png',
|
||||
badge: '/images/icon-72x72.png',
|
||||
tag: data.tag || 'default',
|
||||
data: data.url || '/',
|
||||
actions: [
|
||||
{
|
||||
action: 'open',
|
||||
title: '열기',
|
||||
icon: '/images/icon-open.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: '닫기',
|
||||
icon: '/images/icon-close.png'
|
||||
}
|
||||
],
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
silent: data.silent || false,
|
||||
vibrate: data.vibrate || [200, 100, 200]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click handling
|
||||
self.addEventListener('notificationclick', event => {
|
||||
console.log('Service Worker: Notification clicked', event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'close') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = event.notification.data || '/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
// Check if window is already open
|
||||
for (const client of clientList) {
|
||||
if (client.url === url && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Open new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle messages from main thread
|
||||
self.addEventListener('message', event => {
|
||||
console.log('Service Worker: Message received', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_URLS') {
|
||||
cacheUrls(event.data.urls);
|
||||
}
|
||||
});
|
||||
|
||||
async function cacheUrls(urls) {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
await cache.addAll(urls);
|
||||
console.log('URLs cached successfully:', urls);
|
||||
} catch (error) {
|
||||
console.error('Failed to cache URLs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic background sync (if supported)
|
||||
self.addEventListener('periodicsync', event => {
|
||||
console.log('Service Worker: Periodic sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'content-sync') {
|
||||
event.waitUntil(syncContent());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContent() {
|
||||
try {
|
||||
// Fetch fresh portfolio and services data
|
||||
const portfolioResponse = await fetch('/api/portfolio?featured=true');
|
||||
const servicesResponse = await fetch('/api/services?featured=true');
|
||||
|
||||
if (portfolioResponse.ok && servicesResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
|
||||
cache.put('/api/services?featured=true', servicesResponse.clone());
|
||||
console.log('Content synced successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Content sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache management utilities
|
||||
async function cleanupCaches() {
|
||||
const cacheNames = await caches.keys();
|
||||
const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
|
||||
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (!currentCaches.includes(cacheName)) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Limit cache size
|
||||
async function limitCacheSize(cacheName, maxItems) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
if (keys.length > maxItems) {
|
||||
const keysToDelete = keys.slice(0, keys.length - maxItems);
|
||||
return Promise.all(keysToDelete.map(key => cache.delete(key)));
|
||||
}
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.url.includes('/api/')) {
|
||||
const start = performance.now();
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request).then(response => {
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Log slow API requests
|
||||
if (duration > 2000) {
|
||||
console.warn('Slow API request:', event.request.url, duration + 'ms');
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Error tracking
|
||||
self.addEventListener('error', event => {
|
||||
console.error('Service Worker error:', event.error);
|
||||
// Could send to analytics service
|
||||
});
|
||||
|
||||
self.addEventListener('unhandledrejection', event => {
|
||||
console.error('Service Worker unhandled rejection:', event.reason);
|
||||
// Could send to analytics service
|
||||
});
|
||||
Reference in New Issue
Block a user