From 4dd9192f89791a3959b67dfb069736d3cdbb9adb Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Thu, 11 Sep 2025 20:12:03 +0700 Subject: [PATCH 1/3] feat: implement selective dark mode image treatment and fix footer text size - Add selective dark mode handling for data source images - Create image-remove-bg class for non-transparent backgrounds (INACOVID) - Add image-gov-logo class for selective typography inversion (Dinkes Sulteng) - Keep problematic images unchanged (Sulteng Lawan COVID) - Reduce API URL text size in footer for better mobile display - Add useScrollAnimation composable for reusable scroll animations --- src/components/DataSources.vue | 25 +- src/composables/useScrollAnimation.ts | 73 +++++ src/style.css | 268 +++++++++++++++ src/views/Home.vue | 450 ++++++++++++++++++++------ 4 files changed, 718 insertions(+), 98 deletions(-) create mode 100644 src/composables/useScrollAnimation.ts diff --git a/src/components/DataSources.vue b/src/components/DataSources.vue index dbf0f64..841e809 100644 --- a/src/components/DataSources.vue +++ b/src/components/DataSources.vue @@ -9,7 +9,10 @@ - + @@ -55,4 +58,24 @@ const dataSources: DataSource[] = [ url: 'https://hack.co.id' } ] + +const getImageDarkModeClass = (imagePath: string): string => { + // Images that need background removal (non-transparent white backgrounds) + if (imagePath.includes('inacovid')) { + return 'image-remove-bg' + } + + // Images with government logos that need selective typography color inversion + if (imagePath.includes('dinkes-sulteng')) { + return 'image-gov-logo' + } + + // Images that should remain unchanged in dark mode + if (imagePath.includes('sulteng-lawan-covid')) { + return '' + } + + // Default: apply standard logo dark mode treatment for other logos + return 'logo-dark-mode' +} diff --git a/src/composables/useScrollAnimation.ts b/src/composables/useScrollAnimation.ts new file mode 100644 index 0000000..5ebbfce --- /dev/null +++ b/src/composables/useScrollAnimation.ts @@ -0,0 +1,73 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useScrollAnimation() { + const observers = ref([]) + + const createScrollObserver = ( + elements: NodeListOf | Element[], + options: IntersectionObserverInit = { + threshold: 0.1, + rootMargin: '0px 0px -100px 0px' + } + ) => { + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('in-view') + } + }) + }, options) + + elements.forEach((el) => observer.observe(el)) + observers.value.push(observer) + + return observer + } + + const initScrollAnimations = () => { + // Animate elements on scroll + const animateElements = document.querySelectorAll('.animate-on-scroll') + if (animateElements.length > 0) { + createScrollObserver(animateElements) + } + + // Slide in from left + const slideLeftElements = document.querySelectorAll('.animate-slide-left') + if (slideLeftElements.length > 0) { + createScrollObserver(slideLeftElements) + } + + // Slide in from right + const slideRightElements = document.querySelectorAll('.animate-slide-right') + if (slideRightElements.length > 0) { + createScrollObserver(slideRightElements) + } + + // Stagger animations for child elements + const staggerContainers = document.querySelectorAll('.animate-stagger') + staggerContainers.forEach((container) => { + const children = container.querySelectorAll('.stagger-item') + children.forEach((child, index) => { + (child as HTMLElement).style.animationDelay = `${index * 0.1}s` + }) + }) + } + + const cleanupObservers = () => { + observers.value.forEach(observer => observer.disconnect()) + observers.value = [] + } + + onMounted(() => { + // Small delay to ensure DOM is ready + setTimeout(initScrollAnimations, 100) + }) + + onUnmounted(cleanupObservers) + + return { + initScrollAnimations, + createScrollObserver, + cleanupObservers + } +} \ No newline at end of file diff --git a/src/style.css b/src/style.css index 9abf7b5..383c34b 100644 --- a/src/style.css +++ b/src/style.css @@ -14,6 +14,8 @@ body { margin: 0; font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + @apply bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100; + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; } /* Accessibility improvements */ @@ -225,4 +227,270 @@ body { animation: response-fade-in 0.8s ease-out; animation-delay: 4.3s; animation-fill-mode: both; +} + +/* COVID Virus-themed Animations */ +@keyframes virus-float { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 25% { + transform: translateY(-8px) rotate(90deg); + } + 50% { + transform: translateY(-4px) rotate(180deg); + } + 75% { + transform: translateY(-12px) rotate(270deg); + } +} + +@keyframes molecular-drift { + 0%, 100% { + transform: translateX(0) translateY(0) rotate(0deg) scale(1); + } + 20% { + transform: translateX(4px) translateY(-6px) rotate(45deg) scale(1.05); + } + 40% { + transform: translateX(-2px) translateY(-8px) rotate(90deg) scale(0.95); + } + 60% { + transform: translateX(6px) translateY(-2px) rotate(180deg) scale(1.1); + } + 80% { + transform: translateX(-4px) translateY(-10px) rotate(270deg) scale(0.9); + } +} + +@keyframes organic-pulse { + 0%, 100% { + transform: scale(1) rotate(0deg); + opacity: 0.15; + } + 15% { + transform: scale(1.08) rotate(20deg); + opacity: 0.12; + } + 30% { + transform: scale(0.95) rotate(-10deg); + opacity: 0.18; + } + 45% { + transform: scale(1.12) rotate(35deg); + opacity: 0.10; + } + 60% { + transform: scale(0.9) rotate(-25deg); + opacity: 0.20; + } + 75% { + transform: scale(1.05) rotate(15deg); + opacity: 0.14; + } +} + +@keyframes spiral-rotation { + 0% { + transform: rotate(0deg) translateX(0) translateY(0) scale(1); + } + 25% { + transform: rotate(90deg) translateX(2px) translateY(-2px) scale(1.02); + } + 50% { + transform: rotate(180deg) translateX(0) translateY(-4px) scale(0.98); + } + 75% { + transform: rotate(270deg) translateX(-2px) translateY(-2px) scale(1.04); + } + 100% { + transform: rotate(360deg) translateX(0) translateY(0) scale(1); + } +} + +/* Animation classes */ +.animate-virus-float { + animation: virus-float 6s ease-in-out infinite; +} + +.animate-molecular-drift { + animation: molecular-drift 8s ease-in-out infinite; +} + +.animate-organic-pulse { + animation: organic-pulse 5s ease-in-out infinite; +} + +.animate-spiral-rotation { + animation: spiral-rotation 10s linear infinite; +} + +/* Scroll-driven animations */ +@keyframes reveal-on-scroll { + 0% { + opacity: 0; + transform: translateY(50px) scale(0.9); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes slide-in-left { + 0% { + opacity: 0; + transform: translateX(-100px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slide-in-right { + 0% { + opacity: 0; + transform: translateX(100px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes virus-hover-dance { + 0%, 100% { + transform: translateY(0) rotate(0deg) scale(1); + } + 25% { + transform: translateY(-5px) rotate(10deg) scale(1.05); + } + 50% { + transform: translateY(-8px) rotate(-5deg) scale(1.1); + } + 75% { + transform: translateY(-3px) rotate(8deg) scale(1.02); + } +} + +@keyframes particle-interaction { + 0% { + transform: scale(1) rotate(0deg); + opacity: 0.15; + } + 50% { + transform: scale(1.3) rotate(180deg); + opacity: 0.25; + } + 100% { + transform: scale(1) rotate(360deg); + opacity: 0.15; + } +} + +/* Scroll-driven animation classes */ +.animate-on-scroll { + opacity: 0; + transform: translateY(50px) scale(0.9); + transition: all 0.6s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.animate-on-scroll.in-view { + opacity: 1; + transform: translateY(0) scale(1); +} + +.animate-slide-left { + opacity: 0; + transform: translateX(-100px); + transition: all 0.8s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.animate-slide-left.in-view { + opacity: 1; + transform: translateX(0); +} + +.animate-slide-right { + opacity: 0; + transform: translateX(100px); + transition: all 0.8s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.animate-slide-right.in-view { + opacity: 1; + transform: translateX(0); +} + +/* Hover interaction animations */ +.virus-interactive:hover { + animation: virus-hover-dance 0.8s ease-in-out; +} + +.particle-interactive:hover { + animation: particle-interaction 1.2s ease-in-out; +} + +.feature-card-hover { + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.feature-card-hover:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); +} + +/* Dark mode image styles */ +.dark-mode-invert { + transition: filter 0.3s ease-in-out; +} + +.dark .dark-mode-invert { + filter: invert(1) hue-rotate(180deg); +} + +/* Logo specific dark mode handling */ +.logo-dark-mode { + transition: filter 0.3s ease-in-out; +} + +.dark .logo-dark-mode { + filter: brightness(0) invert(1); +} + +/* Image background removal for better dark mode */ +.image-no-bg { + mix-blend-mode: multiply; + transition: mix-blend-mode 0.3s ease-in-out; +} + +.dark .image-no-bg { + mix-blend-mode: screen; + filter: brightness(1.2) contrast(1.1); +} + +/* Remove white backgrounds from images like INACOVID */ +.image-remove-bg { + transition: mix-blend-mode 0.3s ease-in-out, filter 0.3s ease-in-out; +} + +.dark .image-remove-bg { + mix-blend-mode: screen; + filter: contrast(1.5) brightness(1.1); +} + +/* Government logos with selective typography inversion */ +.image-gov-logo { + transition: filter 0.3s ease-in-out; +} + +.dark .image-gov-logo { + /* This creates a complex filter that attempts to invert only dark text while preserving colored elements */ + filter: + brightness(1.2) + contrast(1.3) + sepia(0.1) + hue-rotate(180deg) + invert(0.15); } \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue index ae89228..56083c5 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,5 +1,5 @@ From b4d950196cac0e4b7f736422be2b97c902f7de41 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Thu, 11 Sep 2025 21:13:25 +0700 Subject: [PATCH 2/3] feat: enhance documentation with persistent sidebar and rt calculation examples - Restructure documentation sidebar with persistent header and footer - Add version display (1.2.0) in sidebar footer with proper attribution - Move theme and language controls to persistent sidebar header - Implement comprehensive dark mode across all documentation components - Add detailed practical Rt calculation example with step-by-step math - Include gamma quantile formulas and confidence interval calculations - Enhance localization with bilingual mathematical terminology - Fix component name from "Banua Coders" to "Banua Coder" - Improve sidebar layout with fixed header, scrollable content, persistent footer - Add mathematical formulas for Wilson-Hilferty approximation and EpiEstim method --- src/App.vue | 71 +++- src/components/CodeBlock.vue | 12 +- src/components/Navigation.vue | 47 ++- src/components/ThemeToggle.vue | 16 +- .../documentation/AuthenticationSection.vue | 50 +-- .../documentation/ErrorHandlingSection.vue | 62 ++-- .../documentation/GlossarySection.vue | 341 ++++++++++++------ .../documentation/HealthCheckSection.vue | 66 ++-- .../NationalHistoricalSection.vue | 194 +++++----- .../documentation/NationalLatestSection.vue | 76 ++-- .../documentation/OverviewSection.vue | 22 +- .../documentation/ProvinceCasesSection.vue | 208 +++++------ .../documentation/ProvincesSection.vue | 46 +-- .../documentation/RootEndpointSection.vue | 46 +-- src/locales/en.json | 72 ++++ src/locales/id.json | 72 ++++ src/views/Documentation.vue | 135 ++++--- 17 files changed, 948 insertions(+), 588 deletions(-) diff --git a/src/App.vue b/src/App.vue index c21583f..27f9908 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,20 +1,28 @@ @@ -23,4 +31,59 @@ onMounted(() => { #app { min-height: 100vh; } + +/* Fade transition */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.25s ease; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +/* Slide transitions */ +.slide-left-enter-active, +.slide-left-leave-active, +.slide-right-enter-active, +.slide-right-leave-active { + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); +} + +.slide-left-enter-from { + opacity: 0; + transform: translateX(30px); +} + +.slide-left-leave-to { + opacity: 0; + transform: translateX(-30px); +} + +.slide-right-enter-from { + opacity: 0; + transform: translateX(-30px); +} + +.slide-right-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* Scale transition */ +.scale-enter-active, +.scale-leave-active { + transition: all 0.25s ease; +} + +.scale-enter-from { + opacity: 0; + transform: scale(0.95); +} + +.scale-leave-to { + opacity: 0; + transform: scale(1.05); +} diff --git a/src/components/CodeBlock.vue b/src/components/CodeBlock.vue index 76cf028..e93ee96 100644 --- a/src/components/CodeBlock.vue +++ b/src/components/CodeBlock.vue @@ -92,6 +92,8 @@ import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-typescript' import 'prismjs/components/prism-python' import 'prismjs/components/prism-go' +import 'prismjs/components/prism-markup' // Must be loaded before PHP +import 'prismjs/components/prism-markup-templating' // Required for PHP import 'prismjs/components/prism-php' import 'prismjs/components/prism-java' import 'prismjs/components/prism-c' @@ -99,7 +101,6 @@ import 'prismjs/components/prism-cpp' import 'prismjs/components/prism-csharp' import 'prismjs/components/prism-css' import 'prismjs/components/prism-scss' -import 'prismjs/components/prism-markup' import 'prismjs/components/prism-yaml' import 'prismjs/components/prism-docker' import 'prismjs/components/prism-sql' @@ -241,13 +242,10 @@ const codeClasses = computed(() => [ const highlightCode = () => { if (codeElement.value) { - // Ensure the language is loaded + // Check if the language is available if (!Prism.languages[detectedLanguage.value] && detectedLanguage.value !== 'text') { - try { - require(`prismjs/components/prism-${detectedLanguage.value}`) - } catch (e) { - console.warn(`Language ${detectedLanguage.value} not available in Prism.js`) - } + console.warn(`Language ${detectedLanguage.value} not available in Prism.js`) + return } Prism.highlightElement(codeElement.value) diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index 72fc1c1..812505e 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -4,7 +4,7 @@
- PICO API Logo + PICO API Logo PICO SulTeng
@@ -14,22 +14,22 @@
{{ t('nav.home') }} {{ t('nav.documentation') }} {{ t('nav.apiReference') }} @@ -38,41 +38,38 @@
- - - + + +
-
- - - +
+ + +