diff --git a/groups/Tapops/css/documents.css b/groups/Tapops/css/documents.css index b4181e7..b0e6e35 100644 --- a/groups/Tapops/css/documents.css +++ b/groups/Tapops/css/documents.css @@ -1,1328 +1 @@ -/* =========================================== - Document Hub & Dashboard Layout - =========================================== */ - -.documents-section { - padding: 120px 5%; - background: hsl(0, 0%, 100%); -} - -.repo-dashboard-container { - max-width: 1200px; - margin: 0 auto; - display: grid; - grid-template-columns: 280px 1fr; - gap: 40px; - align-items: start; -} - -/* --- Sidebar Styling --- */ - -.repo-sidebar { - position: sticky; - top: 150px; -} - -/* Mobile category dropdown wrapper (hidden on desktop) */ -.mobile-category-control-group { - display: none !important; -} - -.sidebar-title { - font-size: 0.75rem; - letter-spacing: 2px; - font-weight: 800; - color: #94a3b8; - margin-bottom: 20px; - text-transform: uppercase; -} - -.document-filter-wrapper { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 30px; -} - -/* Desktop: show buttons, hide dropdown */ -.filter-btn-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.filter-select-group { - display: none; -} - -.doc-filter-btn { - text-align: left; - padding: 14px 18px; - background: white; - border: 1px solid #e2e8f0; - font-size: 0.85rem; - font-weight: 700; - color: var(--primary-blue); - cursor: pointer; - transition: all 0.2s ease; - border-radius: 4px; -} - -.doc-filter-btn.active { - background: var(--primary-blue); - color: white; - border-color: var(--primary-blue); - box-shadow: 0 4px 12px rgba(0, 49, 80, 0.15); -} - -.doc-filter-btn:hover:not(.active) { - border-color: var(--secondary-gold); - background: #fffdf5; -} - -/* --- Custom UI Dropdown --- */ -.custom-dropdown { - position: relative; - width: 100%; -} - -.custom-dropdown-trigger { - width: 100%; - padding: 14px 18px; - background: white; - border: 1px solid #e2e8f0; - border-radius: 4px; - font-family: inherit; - font-size: 0.9rem; - font-weight: 700; - color: var(--primary-blue); - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.custom-dropdown-trigger:hover { - border-color: var(--secondary-gold); - background: #fffdf5; -} - -.custom-dropdown-trigger:focus-visible { - outline: none; - border-color: var(--primary-blue); - box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); -} - -.custom-dropdown-trigger .trigger-arrow { - font-size: 0.8rem; - transition: transform 0.25s ease; - color: var(--primary-blue); -} - -.custom-dropdown.active .custom-dropdown-trigger .trigger-arrow { - transform: rotate(180deg); -} - -.custom-dropdown.active .custom-dropdown-trigger { - border-color: var(--primary-blue); - box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); -} - -.custom-dropdown-menu { - position: absolute; - top: calc(100% + 6px); - left: 0; - right: 0; - background: white; - border: 1px solid #e2e8f0; - border-radius: 6px; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - z-index: 100; - opacity: 0; - visibility: hidden; - transform: translateY(-8px); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - overflow: hidden; - max-height: 300px; - overflow-y: auto; -} - -.custom-dropdown.active .custom-dropdown-menu { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.custom-dropdown-item { - padding: 12px 18px; - font-size: 0.88rem; - font-weight: 600; - color: #475569; - cursor: pointer; - transition: all 0.15s ease; - display: flex; - align-items: center; - justify-content: space-between; -} - -.custom-dropdown-item:hover { - background: #f1f5f9; - color: var(--primary-blue); -} - -.custom-dropdown-item.active { - background: #f0f7ff; - color: var(--primary-blue); - font-weight: 700; -} - -/* Tick mark for selected item */ -.custom-dropdown-item.active::after { - content: "\f00c"; - font-family: "Font Awesome 5 Free"; - font-weight: 900; - font-size: 0.75rem; - color: var(--primary-blue); -} - -/* --- Filter Skeleton Loading --- */ - -.filter-skeleton { - display: flex; - flex-direction: column; - gap: 8px; -} - -.filter-skeleton-item { - height: 48px; - border-radius: 4px; - background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); - background-size: 200% 100%; - animation: skeleton-shimmer 1.5s ease-in-out infinite; -} - -.filter-skeleton-item.short { - width: 70%; -} - -@keyframes skeleton-shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -.sidebar-info-box { - background: #f1f5f9; - padding: 20px; - border-radius: 4px; - display: flex; - gap: 12px; -} - -.sidebar-info-box i { - color: var(--primary-blue); - margin-top: 3px; -} - -.sidebar-info-box p { - font-size: 0.82rem; - color: #64748b; - line-height: 1.5; -} - -/* --- Content Area & Search --- */ - -.repo-main-content { - min-height: 800px; - min-width: 0; -} - -.repo-content-header { - display: flex; - flex-direction: column; - gap: 30px; - margin-bottom: 50px; -} - -.header-title-area .section-title { - margin-bottom: 8px; -} - -.section-subtitle { - font-size: 0.95rem; - color: #64748b; - font-weight: 400; -} - -.repo-controls { - display: flex; - justify-content: space-between; - align-items: flex-end; - gap: 24px; - background: white; - padding: 24px; - border-radius: 8px; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); -} - -.search-container { - flex: 1; - min-width: 260px; - max-width: 500px; -} - -.search-input-group { - position: relative; - width: 100%; -} - -.search-icon { - position: absolute; - left: 18px; - top: 50%; - transform: translateY(-50%); - color: #94a3b8; - font-size: 1rem; -} - -#doc-search { - width: 100%; - padding: 16px 16px 16px 50px; - background: white; - border: 1px solid #e2e8f0; - border-radius: 4px; - font-size: 0.95rem; - color: var(--primary-blue); - transition: all 0.3s ease; -} - -#doc-search:focus { - outline: none; -} - -/* --- Filter Controls --- */ - -.filter-controls { - display: flex; - gap: 16px; - align-items: flex-end; -} - -.control-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.date-range-group { - min-width: 300px; - position: relative; -} - -.control-label { - font-size: 0.7rem; - font-weight: 800; - color: #94a3b8; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.select-wrapper { - position: relative; - min-width: 140px; -} - -#sort-filter-wrapper { - position: relative; - min-width: 140px; -} - -.select-icon { - position: absolute; - left: 14px; - top: 50%; - transform: translateY(-50%); - color: var(--primary-blue); - font-size: 0.8rem; - pointer-events: none; -} - -/* --- Date Range Filter & Popover --- */ -.date-picker-trigger { - width: 100%; - padding: 14px 18px; - background: white; - border: 1px solid var(--border-color); - border-radius: 4px; - font-family: inherit; - font-size: 0.9rem; - font-weight: 700; - color: var(--primary-blue); - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.date-picker-trigger:hover { - border-color: var(--secondary-gold); - background: #fffdf5; -} - -.date-picker-trigger:focus-visible { - outline: none; - border-color: var(--primary-blue); - box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); -} - -.date-picker-trigger .trigger-left-content { - display: flex; - align-items: center; - gap: 10px; -} - -.date-picker-trigger .trigger-arrow { - font-size: 0.8rem; - transition: transform 0.25s ease; - color: var(--primary-blue); -} - -.date-range-group.active .date-picker-trigger .trigger-arrow { - transform: rotate(180deg); -} - -.date-range-group.active .date-picker-trigger { - border-color: var(--primary-blue); - box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); -} - -/* Backdrop */ -.date-picker-backdrop { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(4px); - z-index: 40; -} - -/* Popover */ -.date-picker-popover { - position: absolute; - top: calc(100% + 6px); - right: 0; - width: 630px; - background: white; - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - display: none; - flex-direction: column; - z-index: 45; - overflow: hidden; -} - -.date-range-group.active .date-picker-popover { - display: flex; -} - -/* Top Bar */ -.date-picker-top-bar { - padding: 12px 16px; - border-bottom: 1px solid var(--border-color); - background: #f8fafc; - display: flex; - justify-content: flex-end; -} - -.date-display-inputs { - display: flex; - align-items: center; - gap: 8px; -} - -.date-display-field input { - width: 110px; - padding: 6px 10px; - border: 1px solid #cbd5e1; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 600; - color: var(--primary-blue); - text-align: center; - background: white; -} - -.date-picker-separator { - color: #94a3b8; - font-weight: 700; -} - -/* Body */ -.date-picker-body { - display: flex; - background: white; -} - -/* Calendar Pane */ -.calendar-container { - flex: 1; - padding: 16px; -} - -.calendar-nav { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding: 0; -} - -.month-labels { - display: flex; - gap: 90px; - color: var(--primary-blue); - font-size: 0.95rem; -} - -.month-label { - display: flex; - align-items: center; - gap: 6px; - font-weight: 700; -} - -.year-select-wrapper { - position: relative; - display: inline-flex; - align-items: center; - border-radius: 4px; - transition: background 0.2s ease; -} - -.year-select-wrapper:hover { - background: #f1f5f9; -} - -.calendar-year-select { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - background: transparent; - border: none; - padding: 2px 16px 2px 6px; - font-family: inherit; - font-size: 0.95rem; - font-weight: 700; - color: var(--primary-blue); - cursor: pointer; - outline: none; -} - -.year-select-wrapper .select-arrow { - position: absolute; - right: 4px; - top: 50%; - transform: translateY(-50%); - font-size: 0.6rem; - color: var(--primary-blue); - pointer-events: none; -} - -.nav-btn { - background: transparent; - border: none; - color: #64748b; - cursor: pointer; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: all 0.2s ease; -} - -.nav-btn:hover { - background: #f1f5f9; - color: var(--primary-blue); -} - -.calendar-grids { - display: flex; - gap: 20px; -} - -.calendar-grid-pane { - flex: 1; -} - -.day-headers { - display: grid; - grid-template-columns: repeat(7, 1fr); - text-align: center; - font-size: 0.72rem; - font-weight: 700; - color: #94a3b8; - margin-bottom: 8px; -} - -.days-container { - display: grid; - grid-template-columns: repeat(7, 1fr); - row-gap: 4px; - column-gap: 2px; -} - -.day-cell { - height: 32px; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.82rem; - font-weight: 600; - color: #475569; - cursor: pointer; - border-radius: 4px; - transition: all 0.15s ease; - user-select: none; -} - -.day-cell:hover { - background: #f1f5f9; - color: var(--primary-blue); -} - -.day-cell.empty { - pointer-events: none; - visibility: hidden; -} - -.day-cell.disabled { - color: #cbd5e1; - pointer-events: none; -} - -.day-cell.range-start { - background: var(--primary-blue) !important; - color: white !important; - border-radius: 4px 0 0 4px; -} - -.day-cell.range-end { - background: var(--primary-blue) !important; - color: white !important; - border-radius: 0 4px 4px 0; -} - -.day-cell.range-start.range-end { - border-radius: 4px !important; -} - -.day-cell.range-between { - background: #eff6ff !important; - color: var(--primary-blue) !important; - border-radius: 0; -} - -/* Presets Pane */ -.presets-container { - width: 190px; - border-left: 1px solid var(--border-color); - padding: 16px; - background: white; - display: flex; - flex-direction: column; - gap: 12px; -} - -.presets-title { - font-size: 0.7rem; - font-weight: 800; - color: #94a3b8; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 4px; - display: block; -} - -.presets-list { - display: flex; - flex-direction: column; - gap: 4px; - max-height: 240px; - overflow-y: auto; -} - -.preset-btn { - background: transparent; - border: none; - text-align: left; - padding: 8px 12px; - font-size: 0.8rem; - font-weight: 600; - color: #475569; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: space-between; - font-family: inherit; -} - -.preset-btn:hover { - background: #f8fafc; - color: var(--primary-blue); -} - -.preset-btn.active { - background: #eff6ff; - color: var(--primary-blue); -} - -.preset-btn.active::after { - content: '✓'; - font-weight: 700; - color: var(--primary-blue); - font-size: 0.85rem; -} - -/* Footer Action Buttons */ -.date-picker-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding: 12px 16px; - border-top: 1px solid var(--border-color); - background: #f8fafc; -} - -.action-btn { - padding: 8px 20px; - font-size: 0.85rem; - font-weight: 700; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s ease; - font-family: inherit; -} - -.cancel-btn { - background: white; - border: 1px solid #cbd5e1; - color: #475569; -} - -.cancel-btn:hover { - background: #f1f5f9; - border-color: #94a3b8; - color: #1e293b; -} - -.apply-btn { - background: var(--primary-blue); - border: 1px solid var(--primary-blue); - color: white; -} - -.apply-btn:hover { - background: #002238; - border-color: #002238; -} - -.range-divider { - font-size: 0.7rem; - font-weight: 800; - color: #94a3b8; - text-transform: uppercase; - padding: 0 4px; -} - -.filter-select, -select#sort-filter { - width: 100%; - padding: 12px 12px 12px 36px; - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 4px; - font-size: 0.85rem; - font-weight: 700; - color: var(--primary-blue); - cursor: pointer; - appearance: none; - transition: all 0.2s ease; -} - -.filter-select:hover, -select#sort-filter:hover { - border-color: var(--secondary-gold); - background: white; -} - -.filter-select:focus, -select#sort-filter:focus { - outline: none; -} - -/* --- Document Cards Grid --- */ - -.document-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 24px; -} - -.document-card { - background: white; - border: 1px solid #e2e8f0; - padding: 30px; - display: flex; - flex-direction: column; - transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); - border-radius: 4px; -} - -.document-card:hover { - border-color: var(--secondary-gold); - box-shadow: 0 20px 40px rgba(0, 49, 80, 0.05); - transform: translateY(-5px); -} - -.doc-type-icon { - font-size: 2.2rem; - margin-bottom: 20px; -} - -.doc-type-pdf { - color: #ef4444; -} - -.doc-type-docx { - color: #3b82f6; -} - -.doc-title { - font-size: 1.15rem; - font-weight: 800; - color: var(--primary-blue); - margin-bottom: 12px; - line-height: 1.4; -} - -.doc-meta { - display: flex; - gap: 8px; - margin-bottom: 25px; - flex-wrap: wrap; -} - -.doc-badge { - font-size: 0.65rem; - font-weight: 800; - padding: 4px 10px; - background: #f1f5f9; - color: #64748b; - text-transform: uppercase; -} - -.doc-badge.date { - background: hsl(200, 95%, 95%); - color: hsl(200, 95%, 30%); -} - -/* --- Tag Badges --- */ - -.doc-tags { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - margin-bottom: 20px; -} - -.doc-tags-icon { - font-size: 0.7rem; - color: #94a3b8; - margin-right: 2px; -} - -.doc-tag { - font-size: 0.65rem; - font-weight: 700; - padding: 3px 10px; - background: hsl(43, 100%, 95%); - color: hsl(43, 80%, 35%); - border: 1px solid hsl(43, 80%, 85%); - border-radius: 20px; - text-transform: lowercase; - letter-spacing: 0.3px; - transition: all 0.2s ease; -} - -.doc-tag:hover { - background: hsl(43, 80%, 88%); - border-color: hsl(43, 80%, 70%); -} - -.doc-actions { - margin-top: auto; - display: flex; - flex-direction: column; - gap: 10px; -} - -.btn-action-primary, -.btn-action-outline { - padding: 12px; - text-align: center; - text-decoration: none; - font-weight: 800; - font-size: 0.75rem; - letter-spacing: 0.5px; - text-transform: uppercase; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - transition: all 0.2s ease; - border-radius: 4px; - width: 100%; -} - -a.btn-action-primary { - background: var(--primary-blue); - color: white; - border: 1px solid var(--primary-blue); -} - -a.btn-action-primary:hover { - background: var(--secondary-gold); - border-color: var(--secondary-gold); - color: var(--primary-blue); -} - -a.btn-action-outline { - background: transparent; - color: var(--primary-blue); - border: 1px solid #e2e8f0; -} - -a.btn-action-outline:hover { - border-color: var(--primary-blue); - background: #f8fafc; -} - -/* =========================================== - Responsive — Tablet (≤1024px) - =========================================== */ - -@media (max-width: 1024px) { - .repo-dashboard-container { - grid-template-columns: 1fr; - } - - .repo-sidebar { - display: none; - } - - .mobile-category-control-group { - display: flex !important; - flex-direction: column; - gap: 8px; - } - - /* Mobile: show dropdown */ - .filter-select-group { - display: block; - } - - /* Skeleton single bar on mobile */ - .filter-skeleton { - flex-direction: column; - } - - .filter-skeleton-item { - height: 48px; - } - - .filter-skeleton-item.short { - display: none; - } - - .filter-skeleton-item:not(.short) ~ .filter-skeleton-item:not(.short) { - display: none; - } - - .repo-controls { - flex-direction: column; - align-items: stretch; - } - - .search-container { - max-width: 100%; - } - - .filter-controls { - justify-content: flex-start; - } - - .control-group { - flex: 1; - } -} - -/* =========================================== - Responsive — Tablet / Large Phone (≤768px) - =========================================== */ - -@media (max-width: 768px) { - .documents-section { - padding: 80px 16px; - } - - .repo-content-header { - gap: 20px; - margin-bottom: 30px; - } - - .repo-controls { - padding: 16px; - gap: 16px; - } - - #doc-search { - padding: 14px 14px 14px 44px; - font-size: 0.88rem; - } - - .filter-controls { - flex-direction: column; - gap: 12px; - } - - .control-group { - width: 100%; - } - - .date-range-group { - min-width: 0; - position: relative; - } - - body.date-picker-open { - overflow: hidden; - } - - body.date-picker-open .date-picker-backdrop { - display: block; - z-index: 998; - } - - .date-picker-popover { - /* Default mobile popover positioning inside media query is overridden below, - but let's define the base transitions or layouts. */ - } - - @media (max-width: 768px) { - .date-picker-popover { - position: fixed; - top: 50%; - left: 50%; - right: auto; - transform: translate(-50%, -50%); - width: calc(100% - 32px); - max-width: 380px; - max-height: 90vh; - overflow-y: auto; - border-radius: 12px; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1); - z-index: 999; - } - - .date-picker-top-bar { - justify-content: center; - } - - .calendar-grids { - gap: 0; - } - - #right-calendar-pane { - display: none; - } - - .month-labels { - gap: 0; - justify-content: center; - flex: 1; - } - - #right-month-label { - display: none; - } - - .date-picker-body { - flex-direction: column; - } - - .presets-container { - width: 100%; - border-left: none; - border-top: 1px solid var(--border-color); - padding: 12px 16px; - } - - .presets-list { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 6px; - max-height: none; - } - - .preset-btn { - padding: 6px 10px; - font-size: 0.75rem; - } - } - - .select-wrapper, - #sort-filter-wrapper { - min-width: 0; - width: 100%; - } - - /* Cards: 2-column when space permits, else single */ - .document-grid { - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 16px; - } - - .document-card { - padding: 22px; - } - - .document-card:hover { - transform: none; - } - - .doc-type-icon { - font-size: 1.8rem; - margin-bottom: 14px; - } - - .doc-title { - font-size: 1rem; - } - - .doc-meta { - margin-bottom: 16px; - } - - .doc-tags { - margin-bottom: 14px; - } - - .repo-main-content { - min-height: 400px; - } -} - -/* =========================================== - Responsive — Phone (≤480px) - =========================================== */ - -@media (max-width: 480px) { - .date-dd, .date-mm { - width: 26px; - } - - .date-yyyy { - min-width: 38px; - } - - .date-calendar-btn { - padding-left: 4px; - font-size: 0.82rem; - } -} - -/* =========================================== - Pagination - =========================================== */ - -.doc-pagination { - display: flex; - align-items: center; - justify-content: space-between; - padding: 32px 0 0; - gap: 16px; - flex-wrap: wrap; -} - -.pagination-info { - font-size: 0.75rem; - font-weight: 800; - color: #94a3b8; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.pagination-nav { - display: flex; - align-items: center; - gap: 6px; -} - -.pagination-btn { - min-width: 40px; - height: 40px; - padding: 0 10px; - border: 1px solid #e2e8f0; - background: white; - color: var(--primary-blue); - font-size: 0.85rem; - font-weight: 700; - cursor: pointer; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; - font-family: inherit; -} - -.pagination-btn:hover:not(.disabled):not(.active) { - border-color: var(--secondary-gold); - background: #fffdf5; -} - -.pagination-btn.active { - background: var(--primary-blue); - border-color: var(--primary-blue); - color: white; -} - -.pagination-btn.disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; -} - -.pagination-ellipsis { - min-width: 32px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - color: #94a3b8; - font-weight: 700; - font-size: 1rem; - letter-spacing: 1px; -} - -@media (max-width: 768px) { - .doc-pagination { - flex-direction: column; - align-items: center; - gap: 12px; - } -} - -@media (max-width: 480px) { - .pagination-btn { - min-width: 36px; - height: 36px; - font-size: 0.8rem; - } - - .pagination-nav { - gap: 4px; - } -} - -/* =========================================== - Responsive — Phone (≤480px) - =========================================== */ - -@media (max-width: 480px) { - .documents-section { - padding: 50px 12px; - } - - .custom-dropdown-trigger { - padding: 12px 14px; - font-size: 0.85rem; - } - - /* Single column cards */ - .document-grid { - grid-template-columns: 1fr; - gap: 12px; - } - - .document-card { - padding: 18px; - } - - .doc-title { - font-size: 0.95rem; - margin-bottom: 8px; - } - - .doc-type-icon { - font-size: 1.5rem; - margin-bottom: 10px; - } - - .doc-meta { - margin-bottom: 12px; - } - - .doc-tags { - margin-bottom: 10px; - gap: 4px; - } - - .doc-tag { - font-size: 0.6rem; - padding: 2px 8px; - } - - .btn-action-primary, - .btn-action-outline { - padding: 10px; - font-size: 0.7rem; - } - - .doc-actions { - gap: 8px; - } - - .repo-controls { - padding: 12px; - gap: 12px; - } - - #doc-search { - padding: 12px 12px 12px 40px; - font-size: 0.85rem; - } - - .search-icon { - left: 14px; - font-size: 0.9rem; - } - - .repo-content-header { - gap: 16px; - margin-bottom: 20px; - } -} +@import url("repository/index.css"); diff --git a/groups/Tapops/css/repository/cards.css b/groups/Tapops/css/repository/cards.css new file mode 100644 index 0000000..fdb7f39 --- /dev/null +++ b/groups/Tapops/css/repository/cards.css @@ -0,0 +1,147 @@ +/* --- Document Cards Grid --- */ + +.document-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; +} + +.document-card { + background: white; + border: 1px solid #e2e8f0; + padding: 30px; + display: flex; + flex-direction: column; + transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1); + border-radius: 4px; +} + +.document-card:hover { + border-color: var(--secondary-gold); + box-shadow: 0 20px 40px rgba(0, 49, 80, 0.05); + transform: translateY(-5px); +} + +.doc-type-icon { + font-size: 2.2rem; + margin-bottom: 20px; +} + +.doc-type-pdf { + color: #ef4444; +} + +.doc-type-docx { + color: #3b82f6; +} + +.doc-title { + font-size: 1.15rem; + font-weight: 800; + color: var(--primary-blue); + margin-bottom: 12px; + line-height: 1.4; +} + +.doc-meta { + display: flex; + gap: 8px; + margin-bottom: 25px; + flex-wrap: wrap; +} + +.doc-badge { + font-size: 0.65rem; + font-weight: 800; + padding: 4px 10px; + background: #f1f5f9; + color: #64748b; + text-transform: uppercase; +} + +.doc-badge.date { + background: hsl(200, 95%, 95%); + color: hsl(200, 95%, 30%); +} + +/* --- Tag Badges --- */ + +.doc-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-bottom: 20px; +} + +.doc-tags-icon { + font-size: 0.7rem; + color: #94a3b8; + margin-right: 2px; +} + +.doc-tag { + font-size: 0.65rem; + font-weight: 700; + padding: 3px 10px; + background: hsl(43, 100%, 95%); + color: hsl(43, 80%, 35%); + border: 1px solid hsl(43, 80%, 85%); + border-radius: 20px; + text-transform: lowercase; + letter-spacing: 0.3px; + transition: all 0.2s ease; +} + +.doc-tag:hover { + background: hsl(43, 80%, 88%); + border-color: hsl(43, 80%, 70%); +} + +.doc-actions { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 10px; +} + +.btn-action-primary, +.btn-action-outline { + padding: 12px; + text-align: center; + text-decoration: none; + font-weight: 800; + font-size: 0.75rem; + letter-spacing: 0.5px; + text-transform: uppercase; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s ease; + border-radius: 4px; + width: 100%; +} + +a.btn-action-primary { + background: var(--primary-blue); + color: white; + border: 1px solid var(--primary-blue); +} + +a.btn-action-primary:hover { + background: var(--secondary-gold); + border-color: var(--secondary-gold); + color: var(--primary-blue); +} + +a.btn-action-outline { + background: transparent; + color: var(--primary-blue); + border: 1px solid #e2e8f0; +} + +a.btn-action-outline:hover { + border-color: var(--primary-blue); + background: #f8fafc; +} diff --git a/groups/Tapops/css/repository/controls.css b/groups/Tapops/css/repository/controls.css new file mode 100644 index 0000000..b05577c --- /dev/null +++ b/groups/Tapops/css/repository/controls.css @@ -0,0 +1,117 @@ +/* --- Content Area & Search --- */ + +.repo-main-content { + min-height: 800px; + min-width: 0; +} + +.repo-content-header { + display: flex; + flex-direction: column; + gap: 30px; + margin-bottom: 50px; +} + +.header-title-area .section-title { + margin-bottom: 8px; +} + +.section-subtitle { + font-size: 0.95rem; + color: #64748b; + font-weight: 400; +} + +.repo-controls { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 24px; + background: white; + padding: 24px; + border-radius: 8px; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); +} + +.search-container { + flex: 1; + min-width: 260px; + max-width: 500px; +} + +.search-input-group { + position: relative; + width: 100%; +} + +.search-icon { + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%); + color: #94a3b8; + font-size: 1rem; +} + +#doc-search { + width: 100%; + padding: 16px 16px 16px 50px; + background: white; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-size: 0.95rem; + color: var(--primary-blue); + transition: all 0.3s ease; +} + +#doc-search:focus { + outline: none; +} + +/* --- Filter Controls --- */ + +.filter-controls { + display: flex; + gap: 16px; + align-items: flex-end; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.date-range-group { + min-width: 300px; + position: relative; +} + +.control-label { + font-size: 0.7rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.select-wrapper { + position: relative; + min-width: 140px; +} + +#sort-filter-wrapper { + position: relative; + min-width: 140px; +} + +.select-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--primary-blue); + font-size: 0.8rem; + pointer-events: none; +} diff --git a/groups/Tapops/css/repository/date-picker.css b/groups/Tapops/css/repository/date-picker.css new file mode 100644 index 0000000..9afcb6b --- /dev/null +++ b/groups/Tapops/css/repository/date-picker.css @@ -0,0 +1,422 @@ +/* --- Date Range Filter & Popover --- */ +.date-picker-trigger { + width: 100%; + padding: 14px 18px; + background: white; + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.date-picker-trigger:hover { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.date-picker-trigger:focus-visible { + outline: none; + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +.date-picker-trigger .trigger-left-content { + display: flex; + align-items: center; + gap: 10px; +} + +.date-picker-trigger .trigger-arrow { + font-size: 0.8rem; + transition: transform 0.25s ease; + color: var(--primary-blue); +} + +.date-range-group.active .date-picker-trigger .trigger-arrow { + transform: rotate(180deg); +} + +.date-range-group.active .date-picker-trigger { + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +/* Backdrop */ +.date-picker-backdrop { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 40; +} + +/* Popover */ +.date-picker-popover { + position: absolute; + top: calc(100% + 6px); + right: 0; + width: 630px; + background: white; + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: none; + flex-direction: column; + z-index: 45; + overflow: hidden; +} + +.date-range-group.active .date-picker-popover { + display: flex; +} + +/* Top Bar */ +.date-picker-top-bar { + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + background: #f8fafc; + display: flex; + justify-content: flex-end; +} + +.date-display-inputs { + display: flex; + align-items: center; + gap: 8px; +} + +.date-display-field input { + width: 110px; + padding: 6px 10px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + color: var(--primary-blue); + text-align: center; + background: white; +} + +.date-picker-separator { + color: #94a3b8; + font-weight: 700; +} + +/* Body */ +.date-picker-body { + display: flex; + background: white; +} + +/* Calendar Pane */ +.calendar-container { + flex: 1; + padding: 16px; +} + +.calendar-nav { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding: 0; +} + +.month-labels { + display: flex; + gap: 90px; + color: var(--primary-blue); + font-size: 0.95rem; +} + +.month-label { + display: flex; + align-items: center; + gap: 6px; + font-weight: 700; +} + +.year-select-wrapper { + position: relative; + display: inline-flex; + align-items: center; + border-radius: 4px; + transition: background 0.2s ease; +} + +.year-select-wrapper:hover { + background: #f1f5f9; +} + +.calendar-year-select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background: transparent; + border: none; + padding: 2px 16px 2px 6px; + font-family: inherit; + font-size: 0.95rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + outline: none; +} + +.year-select-wrapper .select-arrow { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 0.6rem; + color: var(--primary-blue); + pointer-events: none; +} + +.nav-btn { + background: transparent; + border: none; + color: #64748b; + cursor: pointer; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; +} + +.nav-btn:hover { + background: #f1f5f9; + color: var(--primary-blue); +} + +.calendar-grids { + display: flex; + gap: 20px; +} + +.calendar-grid-pane { + flex: 1; +} + +.day-headers { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; + font-size: 0.72rem; + font-weight: 700; + color: #94a3b8; + margin-bottom: 8px; +} + +.days-container { + display: grid; + grid-template-columns: repeat(7, 1fr); + row-gap: 4px; + column-gap: 2px; +} + +.day-cell { + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.82rem; + font-weight: 600; + color: #475569; + cursor: pointer; + border-radius: 4px; + transition: all 0.15s ease; + user-select: none; +} + +.day-cell:hover { + background: #f1f5f9; + color: var(--primary-blue); +} + +.day-cell.empty { + pointer-events: none; + visibility: hidden; +} + +.day-cell.disabled { + color: #cbd5e1; + pointer-events: none; +} + +.day-cell.range-start { + background: var(--primary-blue) !important; + color: white !important; + border-radius: 4px 0 0 4px; +} + +.day-cell.range-end { + background: var(--primary-blue) !important; + color: white !important; + border-radius: 0 4px 4px 0; +} + +.day-cell.range-start.range-end { + border-radius: 4px !important; +} + +.day-cell.range-between { + background: #eff6ff !important; + color: var(--primary-blue) !important; + border-radius: 0; +} + +/* Presets Pane */ +.presets-container { + width: 190px; + border-left: 1px solid var(--border-color); + padding: 16px; + background: white; + display: flex; + flex-direction: column; + gap: 12px; +} + +.presets-title { + font-size: 0.7rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + display: block; +} + +.presets-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 240px; + overflow-y: auto; +} + +.preset-btn { + background: transparent; + border: none; + text-align: left; + padding: 8px 12px; + font-size: 0.8rem; + font-weight: 600; + color: #475569; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: space-between; + font-family: inherit; +} + +.preset-btn:hover { + background: #f8fafc; + color: var(--primary-blue); +} + +.preset-btn.active { + background: #eff6ff; + color: var(--primary-blue); +} + +.preset-btn.active::after { + content: '✓'; + font-weight: 700; + color: var(--primary-blue); + font-size: 0.85rem; +} + +/* Footer Action Buttons */ +.date-picker-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 12px 16px; + border-top: 1px solid var(--border-color); + background: #f8fafc; +} + +.action-btn { + padding: 8px 20px; + font-size: 0.85rem; + font-weight: 700; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.cancel-btn { + background: white; + border: 1px solid #cbd5e1; + color: #475569; +} + +.cancel-btn:hover { + background: #f1f5f9; + border-color: #94a3b8; + color: #1e293b; +} + +.apply-btn { + background: var(--primary-blue); + border: 1px solid var(--primary-blue); + color: white; +} + +.apply-btn:hover { + background: #002238; + border-color: #002238; +} + +.range-divider { + font-size: 0.7rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + padding: 0 4px; +} + +.filter-select, +select#sort-filter { + width: 100%; + padding: 12px 12px 12px 36px; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + appearance: none; + transition: all 0.2s ease; +} + +.filter-select:hover, +select#sort-filter:hover { + border-color: var(--secondary-gold); + background: white; +} + +.filter-select:focus, +select#sort-filter:focus { + outline: none; +} diff --git a/groups/Tapops/css/repository/dropdown.css b/groups/Tapops/css/repository/dropdown.css new file mode 100644 index 0000000..81f0b80 --- /dev/null +++ b/groups/Tapops/css/repository/dropdown.css @@ -0,0 +1,150 @@ +/* --- Custom UI Dropdown --- */ +.custom-dropdown { + position: relative; + width: 100%; +} + +.custom-dropdown-trigger { + width: 100%; + padding: 14px 18px; + background: white; + border: 1px solid #e2e8f0; + border-radius: 4px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.custom-dropdown-trigger:hover { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.custom-dropdown-trigger:focus-visible { + outline: none; + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +.custom-dropdown-trigger .trigger-arrow { + font-size: 0.8rem; + transition: transform 0.25s ease; + color: var(--primary-blue); +} + +.custom-dropdown.active .custom-dropdown-trigger .trigger-arrow { + transform: rotate(180deg); +} + +.custom-dropdown.active .custom-dropdown-trigger { + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.08); +} + +.custom-dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + background: white; + border: 1px solid #e2e8f0; + border-radius: 6px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + z-index: 100; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; + max-height: 300px; + overflow-y: auto; +} + +.custom-dropdown.active .custom-dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.custom-dropdown-item { + padding: 12px 18px; + font-size: 0.88rem; + font-weight: 600; + color: #475569; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: space-between; +} + +.custom-dropdown-item:hover { + background: #f1f5f9; + color: var(--primary-blue); +} + +.custom-dropdown-item.active { + background: #f0f7ff; + color: var(--primary-blue); + font-weight: 700; +} + +/* Tick mark for selected item */ +.custom-dropdown-item.active::after { + content: "\f00c"; + font-family: "Font Awesome 5 Free"; + font-weight: 900; + font-size: 0.75rem; + color: var(--primary-blue); +} + +/* --- Filter Skeleton Loading --- */ + +.filter-skeleton { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-skeleton-item { + height: 48px; + border-radius: 4px; + background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; +} + +.filter-skeleton-item.short { + width: 70%; +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.sidebar-info-box { + background: #f1f5f9; + padding: 20px; + border-radius: 4px; + display: flex; + gap: 12px; +} + +.sidebar-info-box i { + color: var(--primary-blue); + margin-top: 3px; +} + +.sidebar-info-box p { + font-size: 0.82rem; + color: #64748b; + line-height: 1.5; +} diff --git a/groups/Tapops/css/repository/index.css b/groups/Tapops/css/repository/index.css new file mode 100644 index 0000000..67ad9e4 --- /dev/null +++ b/groups/Tapops/css/repository/index.css @@ -0,0 +1,7 @@ +@import url("layout.css"); +@import url("dropdown.css"); +@import url("controls.css"); +@import url("date-picker.css"); +@import url("cards.css"); +@import url("responsive.css"); +@import url("pagination.css"); diff --git a/groups/Tapops/css/repository/layout.css b/groups/Tapops/css/repository/layout.css new file mode 100644 index 0000000..6bf710f --- /dev/null +++ b/groups/Tapops/css/repository/layout.css @@ -0,0 +1,81 @@ +/* =========================================== + Document Hub & Dashboard Layout + =========================================== */ + +.documents-section { + padding: 120px 5%; + background: hsl(0, 0%, 100%); +} + +.repo-dashboard-container { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 280px 1fr; + gap: 40px; + align-items: start; +} + +/* --- Sidebar Styling --- */ + +.repo-sidebar { + position: sticky; + top: 150px; +} + +/* Mobile category dropdown wrapper (hidden on desktop) */ +.mobile-category-control-group { + display: none !important; +} + +.sidebar-title { + font-size: 0.75rem; + letter-spacing: 2px; + font-weight: 800; + color: #94a3b8; + margin-bottom: 20px; + text-transform: uppercase; +} + +.document-filter-wrapper { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 30px; +} + +/* Desktop: show buttons, hide dropdown */ +.filter-btn-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-select-group { + display: none; +} + +.doc-filter-btn { + text-align: left; + padding: 14px 18px; + background: white; + border: 1px solid #e2e8f0; + font-size: 0.85rem; + font-weight: 700; + color: var(--primary-blue); + cursor: pointer; + transition: all 0.2s ease; + border-radius: 4px; +} + +.doc-filter-btn.active { + background: var(--primary-blue); + color: white; + border-color: var(--primary-blue); + box-shadow: 0 4px 12px rgba(0, 49, 80, 0.15); +} + +.doc-filter-btn:hover:not(.active) { + border-color: var(--secondary-gold); + background: #fffdf5; +} diff --git a/groups/Tapops/css/repository/pagination.css b/groups/Tapops/css/repository/pagination.css new file mode 100644 index 0000000..6160cc2 --- /dev/null +++ b/groups/Tapops/css/repository/pagination.css @@ -0,0 +1,172 @@ +/* =========================================== + Pagination + =========================================== */ + +.doc-pagination { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32px 0 0; + gap: 16px; + flex-wrap: wrap; +} + +.pagination-info { + font-size: 0.75rem; + font-weight: 800; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.pagination-nav { + display: flex; + align-items: center; + gap: 6px; +} + +.pagination-btn { + min-width: 40px; + height: 40px; + padding: 0 10px; + border: 1px solid #e2e8f0; + background: white; + color: var(--primary-blue); + font-size: 0.85rem; + font-weight: 700; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-family: inherit; +} + +.pagination-btn:hover:not(.disabled):not(.active) { + border-color: var(--secondary-gold); + background: #fffdf5; +} + +.pagination-btn.active { + background: var(--primary-blue); + border-color: var(--primary-blue); + color: white; +} + +.pagination-btn.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-ellipsis { + min-width: 32px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: #94a3b8; + font-weight: 700; + font-size: 1rem; + letter-spacing: 1px; +} + +@media (max-width: 768px) { + .doc-pagination { + flex-direction: column; + align-items: center; + gap: 12px; + } +} + +@media (max-width: 480px) { + .pagination-btn { + min-width: 36px; + height: 36px; + font-size: 0.8rem; + } + + .pagination-nav { + gap: 4px; + } +} + +/* =========================================== + Responsive — Phone (≤480px) + =========================================== */ + +@media (max-width: 480px) { + .documents-section { + padding: 50px 12px; + } + + .custom-dropdown-trigger { + padding: 12px 14px; + font-size: 0.85rem; + } + + /* Single column cards */ + .document-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .document-card { + padding: 18px; + } + + .doc-title { + font-size: 0.95rem; + margin-bottom: 8px; + } + + .doc-type-icon { + font-size: 1.5rem; + margin-bottom: 10px; + } + + .doc-meta { + margin-bottom: 12px; + } + + .doc-tags { + margin-bottom: 10px; + gap: 4px; + } + + .doc-tag { + font-size: 0.6rem; + padding: 2px 8px; + } + + .btn-action-primary, + .btn-action-outline { + padding: 10px; + font-size: 0.7rem; + } + + .doc-actions { + gap: 8px; + } + + .repo-controls { + padding: 12px; + gap: 12px; + } + + #doc-search { + padding: 12px 12px 12px 40px; + font-size: 0.85rem; + } + + .search-icon { + left: 14px; + font-size: 0.9rem; + } + + .repo-content-header { + gap: 16px; + margin-bottom: 20px; + } +} diff --git a/groups/Tapops/css/repository/responsive.css b/groups/Tapops/css/repository/responsive.css new file mode 100644 index 0000000..636a019 --- /dev/null +++ b/groups/Tapops/css/repository/responsive.css @@ -0,0 +1,233 @@ +/* =========================================== + Responsive — Tablet (≤1024px) + =========================================== */ + +@media (max-width: 1024px) { + .repo-dashboard-container { + grid-template-columns: 1fr; + } + + .repo-sidebar { + display: none; + } + + .mobile-category-control-group { + display: flex !important; + flex-direction: column; + gap: 8px; + } + + /* Mobile: show dropdown */ + .filter-select-group { + display: block; + } + + /* Skeleton single bar on mobile */ + .filter-skeleton { + flex-direction: column; + } + + .filter-skeleton-item { + height: 48px; + } + + .filter-skeleton-item.short { + display: none; + } + + .filter-skeleton-item:not(.short) ~ .filter-skeleton-item:not(.short) { + display: none; + } + + .repo-controls { + flex-direction: column; + align-items: stretch; + } + + .search-container { + max-width: 100%; + } + + .filter-controls { + justify-content: flex-start; + } + + .control-group { + flex: 1; + } +} + +/* =========================================== + Responsive — Tablet / Large Phone (≤768px) + =========================================== */ + +@media (max-width: 768px) { + .documents-section { + padding: 80px 16px; + } + + .repo-content-header { + gap: 20px; + margin-bottom: 30px; + } + + .repo-controls { + padding: 16px; + gap: 16px; + } + + #doc-search { + padding: 14px 14px 14px 44px; + font-size: 0.88rem; + } + + .filter-controls { + flex-direction: column; + gap: 12px; + } + + .control-group { + width: 100%; + } + + .date-range-group { + min-width: 0; + position: relative; + } + + body.date-picker-open { + overflow: hidden; + } + + body.date-picker-open .date-picker-backdrop { + display: block; + z-index: 998; + } + + .date-picker-popover { + /* Default mobile popover positioning inside media query is overridden below, + but let's define the base transitions or layouts. */ + } + + @media (max-width: 768px) { + .date-picker-popover { + position: fixed; + top: 50%; + left: 50%; + right: auto; + transform: translate(-50%, -50%); + width: calc(100% - 32px); + max-width: 380px; + max-height: 90vh; + overflow-y: auto; + border-radius: 12px; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1); + z-index: 999; + } + + .date-picker-top-bar { + justify-content: center; + } + + .calendar-grids { + gap: 0; + } + + #right-calendar-pane { + display: none; + } + + .month-labels { + gap: 0; + justify-content: center; + flex: 1; + } + + #right-month-label { + display: none; + } + + .date-picker-body { + flex-direction: column; + } + + .presets-container { + width: 100%; + border-left: none; + border-top: 1px solid var(--border-color); + padding: 12px 16px; + } + + .presets-list { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; + max-height: none; + } + + .preset-btn { + padding: 6px 10px; + font-size: 0.75rem; + } + } + + .select-wrapper, + #sort-filter-wrapper { + min-width: 0; + width: 100%; + } + + /* Cards: 2-column when space permits, else single */ + .document-grid { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 16px; + } + + .document-card { + padding: 22px; + } + + .document-card:hover { + transform: none; + } + + .doc-type-icon { + font-size: 1.8rem; + margin-bottom: 14px; + } + + .doc-title { + font-size: 1rem; + } + + .doc-meta { + margin-bottom: 16px; + } + + .doc-tags { + margin-bottom: 14px; + } + + .repo-main-content { + min-height: 400px; + } +} + +/* =========================================== + Responsive — Phone (≤480px) + =========================================== */ + +@media (max-width: 480px) { + .date-dd, .date-mm { + width: 26px; + } + + .date-yyyy { + min-width: 38px; + } + + .date-calendar-btn { + padding-left: 4px; + font-size: 0.82rem; + } +} diff --git a/groups/Tapops/index.html b/groups/Tapops/index.html index d3d1b66..28d4c3d 100644 --- a/groups/Tapops/index.html +++ b/groups/Tapops/index.html @@ -11,9 +11,7 @@ - - @@ -22,1307 +20,11 @@ +
+ - - - - - - -
- - -
-
- Digital Administrative Hub -

Administrasi Akademik Digital

-

- Satu pintu menuju semua dokumen penting. Akses terpusat menuju repositori dokumen akademik Program Studi - Informatika Universitas Tanjungpura untuk mahasiswa dan staf. -

- -
- -
-
- - Dokumen Aktif -
-
-
- - Pembaruan Terbaru -
-
-
- - -
-
- - - - -
-
-
-

Eksplorasi Dokumen Prodi

-

Temukan berkas administrasi dan akademik terbaru Program Studi - Informatika.

-
- -
-
-
- - -
-
- -
-
- Periode Unggahan - - -
- -
-
-
-
- -
- - -
- -
-
-
- -
-
-
- -
-
- Mei -
- - -
-
-
- Juni -
- - -
-
-
- -
- -
-
-
-
-
-
-
-
-
-
-
- -
- RENTANG WAKTU -
- - - - - - - - - - -
-
-
- - -
-
- -
- Urutkan -
-
- -
- Kategori Berkas -
-
-
-
-
- -
-
- - Menghubungkan ke repositori berkas... -
-
-
-
-
- - -
-
-
-

Tata Cara Pengajuan Berkas

- -
- -
- -
-
-
-
01
-
-
-
-

Unduh Formulir

-

Pilih kategori pengajuan Anda, lalu unduh formulir versi terbaru. Pastikan kode revisi pada formulir sesuai dengan yang tertera di halaman ini.

-
-
- - -
-
-
-
02
-
-
-
-

Lengkapi Formulir

-

Isi setiap kolom dengan lengkap dan teliti. Sertakan seluruh dokumen pendukung yang diminta sebelum pengiriman.

-
-
- - -
-
-
-
03
-
-
-

Kirim Berkas

-

Kirimkan formulir beserta dokumen pendukung melalui email resmi atau WhatsApp admin prodi. Berkas yang lengkap akan langsung diproses oleh admin prodi.

-
-
-
-
-
- - -
-
-
-

Butuh Bantuan Lebih Lanjut?

-

Tim admin kami siap membantu Anda melalui kanal komunikasi resmi di bawah ini.

-
- -
-
-
-
-

Korespondensi Resmi (Email)

-

Gunakan untuk permohonan form khusus non-publik atau administrasi formal lainnya.

-
    -
  • Respon Maks. 24 Jam
  • -
  • Pelayanan Formal
  • -
- HUBUNGI ADMIN -
-
- -
-
-
-

Quick Support (WhatsApp)

-

Konsultasi cepat alur birokrasi atau pelaporan kendala akses pada berkas repositori.

-
    -
  • Respon Instan Jam Kerja
  • -
  • Konsultasi Teknis
  • -
- CHAT WHATSAPP -
-
-
-
-
- -
- - - - - - - - - - + + + - \ No newline at end of file + diff --git a/groups/Tapops/js/app.js b/groups/Tapops/js/app.js new file mode 100644 index 0000000..b8370a2 --- /dev/null +++ b/groups/Tapops/js/app.js @@ -0,0 +1,28 @@ +import { CounterAnimation } from './components/counter-animation.js'; +import { AccessibilityToggleController } from './controllers/accessibility-toggle-controller.js'; +import { DocumentRepository } from './controllers/document-repository.js'; +import { MobileMenuController } from './controllers/mobile-menu-controller.js'; +import { NavigationController } from './controllers/navigation-controller.js'; +import { pageTemplate } from './templates/page-template.js'; + +class TapopsApp { + constructor(root = document.getElementById('tapops-page-root')) { + this.root = root; + } + + init() { + if (!this.root) return; + + this.root.innerHTML = pageTemplate(); + window.applyLang?.(); + window.applyTp?.(this.root); + + new NavigationController().init(); + new MobileMenuController().init(); + new AccessibilityToggleController().init(); + new CounterAnimation().init(); + new DocumentRepository().init(); + } +} + +new TapopsApp().init(); diff --git a/groups/Tapops/js/components/counter-animation.js b/groups/Tapops/js/components/counter-animation.js new file mode 100644 index 0000000..3639b4f --- /dev/null +++ b/groups/Tapops/js/components/counter-animation.js @@ -0,0 +1,44 @@ +export class CounterAnimation { + static DURATION_MS = 2000; + + init() { + const counters = document.querySelectorAll('.counter-value[data-target]'); + if (counters.length === 0) return; + + if ('IntersectionObserver' in window) { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this._animate(entry.target); + observer.unobserve(entry.target); + } + }); + }, { threshold: 0.2 }); + counters.forEach(c => observer.observe(c)); + } else { + counters.forEach(c => this._animate(c)); + } + } + + _animate(el) { + const target = Number.parseInt((el.dataset.target || '0').replace(/[^0-9]/g, ''), 10); + if (Number.isNaN(target)) return; + CounterAnimation.animateValue(el, target, el.dataset.suffix || ''); + } + + // Reusable easing tween from 0 → target. Callable for stats whose + // value is only known after an async fetch (no data-target needed). + static animateValue(el, target, suffix = '') { + if (!el || Number.isNaN(target)) return; + const startTime = performance.now(); + + const tick = (now) => { + const progress = Math.min((now - startTime) / CounterAnimation.DURATION_MS, 1); + const eased = 1 - Math.pow(1 - progress, 3); + el.textContent = Math.floor(eased * target) + suffix; + if (progress < 1) requestAnimationFrame(tick); + else el.textContent = target + suffix; + }; + requestAnimationFrame(tick); + } +} diff --git a/groups/Tapops/js/components/custom-dropdown.js b/groups/Tapops/js/components/custom-dropdown.js new file mode 100644 index 0000000..a896c36 --- /dev/null +++ b/groups/Tapops/js/components/custom-dropdown.js @@ -0,0 +1,175 @@ +export class CustomDropdown { + constructor({ container, options, defaultValue, onChange, prefixIcon }) { + this.container = container; + this.options = options; // Array of { value, text, i18nKey } + this.onChange = onChange; + this.currentValue = defaultValue || options[0]?.value; + this.prefixIcon = prefixIcon; // optional class name like 'fas fa-sort' + this.isOpen = false; + + this.elements = {}; + this._build(); + this._bindEvents(); + } + + _build() { + this.container.innerHTML = ''; + this.container.classList.add('custom-dropdown'); + + // Trigger Button + const trigger = document.createElement('button'); + trigger.className = 'custom-dropdown-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + trigger.type = 'button'; + + const triggerLeft = document.createElement('div'); + triggerLeft.className = 'trigger-left-content'; + triggerLeft.style.display = 'flex'; + triggerLeft.style.alignItems = 'center'; + triggerLeft.style.gap = '10px'; + + if (this.prefixIcon) { + const icon = document.createElement('i'); + icon.className = this.prefixIcon; + triggerLeft.appendChild(icon); + } + + const labelSpan = document.createElement('span'); + labelSpan.className = 'trigger-label'; + + // Find default text + const defaultOpt = this.options.find(opt => opt.value === this.currentValue) || this.options[0]; + labelSpan.textContent = defaultOpt ? defaultOpt.text : ''; + if (defaultOpt?.i18nKey) { + labelSpan.setAttribute('data-tp-i18n', defaultOpt.i18nKey); + } + + triggerLeft.appendChild(labelSpan); + trigger.appendChild(triggerLeft); + + const arrowIcon = document.createElement('i'); + arrowIcon.className = 'fas fa-chevron-down trigger-arrow'; + + trigger.appendChild(arrowIcon); + this.container.appendChild(trigger); + + // Menu wrapper + const menu = document.createElement('div'); + menu.className = 'custom-dropdown-menu'; + menu.setAttribute('role', 'listbox'); + + // Populate options + this.options.forEach(opt => { + const item = document.createElement('div'); + item.className = 'custom-dropdown-item' + (opt.value === this.currentValue ? ' active' : ''); + item.setAttribute('role', 'option'); + item.setAttribute('data-value', opt.value); + + const itemText = document.createElement('span'); + itemText.textContent = opt.text; + if (opt.i18nKey) { + itemText.setAttribute('data-tp-i18n', opt.i18nKey); + } + + item.appendChild(itemText); + menu.appendChild(item); + }); + + this.container.appendChild(menu); + + this.elements = { trigger, labelSpan, menu }; + } + + _bindEvents() { + // Toggle dropdown on trigger click + this.elements.trigger.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggle(); + }); + + // Handle item selection + this.elements.menu.addEventListener('click', (e) => { + const item = e.target.closest('.custom-dropdown-item'); + if (!item) return; + + const value = item.getAttribute('data-value'); + this.select(value); + this.close(); + }); + + // Close when clicking outside + document.addEventListener('click', () => { + this.close(); + }); + + // Close on escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') this.close(); + }); + } + + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + // Close other custom dropdowns + document.querySelectorAll('.custom-dropdown.active').forEach(dropdown => { + if (dropdown !== this.container) { + dropdown.classList.remove('active'); + dropdown.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false'); + } + }); + + this.isOpen = true; + this.container.classList.add('active'); + this.elements.trigger.setAttribute('aria-expanded', 'true'); + } + + close() { + this.isOpen = false; + this.container.classList.remove('active'); + this.elements.trigger.setAttribute('aria-expanded', 'false'); + } + + select(value) { + if (value === this.currentValue) return; + + this.currentValue = value; + + // Update active class in menu items + this.elements.menu.querySelectorAll('.custom-dropdown-item').forEach(item => { + const isCurrent = item.getAttribute('data-value') === value; + item.classList.toggle('active', isCurrent); + }); + + // Update trigger label + const selectedOpt = this.options.find(opt => opt.value === value); + if (selectedOpt) { + this.elements.labelSpan.textContent = selectedOpt.text; + if (selectedOpt.i18nKey) { + this.elements.labelSpan.setAttribute('data-tp-i18n', selectedOpt.i18nKey); + } else { + this.elements.labelSpan.removeAttribute('data-tp-i18n'); + } + } + + // Trigger callback + if (typeof this.onChange === 'function') { + this.onChange(value); + } + } + + setValue(value) { + this.select(value); + } + + getValue() { + return this.currentValue; + } +} diff --git a/groups/Tapops/js/components/date-range-picker.js b/groups/Tapops/js/components/date-range-picker.js new file mode 100644 index 0000000..2a9902d --- /dev/null +++ b/groups/Tapops/js/components/date-range-picker.js @@ -0,0 +1,663 @@ +export class DateRangePicker { + constructor({ containerId, onChange, initialStartDate = '', initialEndDate = '' }) { + this.container = document.getElementById(containerId); + if (!this.container) return; + + this.trigger = this.container.querySelector('#date-picker-trigger'); + this.popover = this.container.querySelector('#date-picker-popover'); + this.backdrop = this.container.querySelector('#date-picker-backdrop'); + this.label = this.container.querySelector('#date-picker-label'); + + this.startDisplay = this.container.querySelector('#picker-start-display'); + this.endDisplay = this.container.querySelector('#picker-end-display'); + + this.prevBtn = this.container.querySelector('#prev-month-btn'); + this.nextBtn = this.container.querySelector('#next-month-btn'); + this.leftMonthLabel = this.container.querySelector('#left-month-label'); + this.rightMonthLabel = this.container.querySelector('#right-month-label'); + + this.leftMonthName = this.container.querySelector('#left-month-name'); + this.rightMonthName = this.container.querySelector('#right-month-name'); + this.leftYearSelect = this.container.querySelector('#left-year-select'); + this.rightYearSelect = this.container.querySelector('#right-year-select'); + + this.leftPane = this.container.querySelector('#left-calendar-pane'); + this.rightPane = this.container.querySelector('#right-calendar-pane'); + + this.leftDaysContainer = this.container.querySelector('#left-days-container'); + this.rightDaysContainer = this.container.querySelector('#right-days-container'); + + this.cancelBtn = this.container.querySelector('#picker-cancel-btn'); + this.applyBtn = this.container.querySelector('#picker-apply-btn'); + + this.onChange = onChange; + + // Active dates (saved) + this.startDate = initialStartDate ? new Date(initialStartDate) : null; + this.endDate = initialEndDate ? new Date(initialEndDate) : null; + + // Temporary dates (being edited) + this.tempStartDate = this.startDate ? new Date(this.startDate) : null; + this.tempEndDate = this.endDate ? new Date(this.endDate) : null; + + // Calendar view month/year + const today = new Date(); + if (this.endDate) { + this.currentLeftMonth = this.endDate.getMonth(); + this.currentLeftYear = this.endDate.getFullYear(); + } else { + this.currentLeftMonth = today.getMonth(); + this.currentLeftYear = today.getFullYear(); + } + + this.isOpen = false; + this._init(); + } + + _init() { + this._populateYearSelects(); + this._bindEvents(); + this._bindDisplayInputs(); + this._updateTriggerLabel(); + + // Listen to language changes + document.addEventListener('languageChanged', () => { + if (this.isOpen) { + this._renderCalendars(); + } + this._updateTriggerLabel(); + }); + } + + _isEnglish() { + if (typeof window.getLang === 'function') { + const l = window.getLang(); + if (l === 'id' || l === 'en') return l === 'en'; + } + try { + const saved = localStorage.getItem('if_untan_lang'); + if (saved === 'id' || saved === 'en') return saved === 'en'; + } catch (e) {} + const nav = (navigator.language || 'id').toLowerCase(); + return nav.startsWith('en'); + } + + _clearTime(date) { + if (!date) return null; + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; + } + + _datesEqual(d1, d2) { + if (!d1 || !d2) return false; + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + } + + _getRightMonthYear() { + let m = this.currentLeftMonth + 1; + let y = this.currentLeftYear; + if (m > 11) { + m = 0; + y += 1; + } + return { month: m, year: y }; + } + + _navigateMonths(direction) { + this.currentLeftMonth += direction; + if (this.currentLeftMonth > 11) { + this.currentLeftMonth = 0; + this.currentLeftYear += 1; + } else if (this.currentLeftMonth < 0) { + this.currentLeftMonth = 11; + this.currentLeftYear -= 1; + } + this._renderCalendars(); + } + + _renderCalendars() { + const isEn = this._isEnglish(); + const monthNames = isEn ? + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] : + ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']; + + const dayHeaders = isEn ? + ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] : + ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']; + + if (this.leftMonthName) { + this.leftMonthName.textContent = monthNames[this.currentLeftMonth]; + } + if (this.leftYearSelect) { + this.leftYearSelect.value = this.currentLeftYear; + } + + const rightMY = this._getRightMonthYear(); + if (this.rightMonthName) { + this.rightMonthName.textContent = monthNames[rightMY.month]; + } + if (this.rightYearSelect) { + this.rightYearSelect.value = rightMY.year; + } + + const drawHeaders = (pane) => { + if (!pane) return; + const dayHeadersEl = pane.querySelector('.day-headers'); + if (!dayHeadersEl) return; + dayHeadersEl.innerHTML = ''; + dayHeaders.forEach(day => { + const span = document.createElement('span'); + span.textContent = day; + dayHeadersEl.appendChild(span); + }); + }; + + drawHeaders(this.leftPane); + drawHeaders(this.rightPane); + + if (this.leftDaysContainer) { + this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear); + } + if (this.rightDaysContainer) { + this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year); + } + + this._updateDisplayInputs(); + this._updatePresetSelectionVisuals(); + } + + _populateDaysContainer(container, month, year) { + container.innerHTML = ''; + + const firstDay = new Date(year, month, 1); + const numDays = new Date(year, month + 1, 0).getDate(); + + let offset = (firstDay.getDay() + 6) % 7; + + for (let i = 0; i < offset; i++) { + const cell = document.createElement('div'); + cell.className = 'day-cell empty'; + container.appendChild(cell); + } + + const today = new Date(); + today.setHours(23, 59, 59, 999); + + for (let day = 1; day <= numDays; day++) { + const cell = document.createElement('div'); + cell.className = 'day-cell'; + cell.textContent = day; + + const cellDate = new Date(year, month, day); + cellDate.setHours(0, 0, 0, 0); + + if (cellDate > today) { + cell.classList.add('disabled'); + } else { + if (this.tempStartDate && this.tempEndDate) { + const start = this._clearTime(this.tempStartDate); + const end = this._clearTime(this.tempEndDate); + + if (this._datesEqual(cellDate, start)) { + cell.classList.add('range-start'); + } + if (this._datesEqual(cellDate, end)) { + cell.classList.add('range-end'); + } + if (cellDate > start && cellDate < end) { + cell.classList.add('range-between'); + } + } else if (this.tempStartDate && this._datesEqual(cellDate, this._clearTime(this.tempStartDate))) { + cell.classList.add('range-start'); + cell.classList.add('range-end'); + } + + cell.addEventListener('click', () => { + this._handleDayClick(cellDate); + }); + } + + container.appendChild(cell); + } + } + + _handleDayClick(date) { + if (!this.tempStartDate || (this.tempStartDate && this.tempEndDate)) { + this.tempStartDate = new Date(date); + this.tempEndDate = null; + } else { + if (date < this.tempStartDate) { + this.tempStartDate = new Date(date); + this.tempEndDate = null; + } else { + this.tempEndDate = new Date(date); + } + } + this._renderCalendars(); + } + + _formatDateDisplay(date) { + if (!date) return ''; + const d = new Date(date); + const dd = String(d.getDate()).padStart(2, '0'); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const yyyy = d.getFullYear(); + return `${dd}/${mm}/${yyyy}`; + } + + _updateDisplayInputs() { + if (this.startDisplay) { + this.startDisplay.value = this._formatDateDisplay(this.tempStartDate); + } + if (this.endDisplay) { + this.endDisplay.value = this._formatDateDisplay(this.tempEndDate); + } + } + + _calculatePreset(presetName) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + let start = null; + let end = null; + + switch (presetName) { + case 'today': + start = new Date(today); + end = new Date(today); + break; + case 'yesterday': + start = new Date(today); + start.setDate(today.getDate() - 1); + end = new Date(start); + break; + case 'last7': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 6); + break; + case 'last14': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 13); + break; + case 'last30': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 29); + break; + case 'thisWeek': + end = new Date(today); + start = new Date(today); + const day = today.getDay(); + const diff = today.getDate() - day + (day === 0 ? -6 : 1); + start.setDate(diff); + break; + case 'lastWeek': + const temp = new Date(today); + const tempDay = temp.getDay(); + const tempDiff = temp.getDate() - tempDay + (tempDay === 0 ? -6 : 1); + start = new Date(temp); + start.setDate(tempDiff - 7); + end = new Date(temp); + end.setDate(tempDiff - 1); + break; + case 'thisMonth': + start = new Date(today.getFullYear(), today.getMonth(), 1); + end = new Date(today); + break; + case 'lastMonth': + start = new Date(today.getFullYear(), today.getMonth() - 1, 1); + end = new Date(today.getFullYear(), today.getMonth(), 0); + break; + case 'all': + default: + start = null; + end = null; + break; + } + return { start, end }; + } + + _handlePresetClick(presetName) { + const { start, end } = this._calculatePreset(presetName); + this.tempStartDate = start; + this.tempEndDate = end; + + const today = new Date(); + if (this.tempEndDate) { + this.currentLeftMonth = this.tempEndDate.getMonth(); + this.currentLeftYear = this.tempEndDate.getFullYear(); + } else { + this.currentLeftMonth = today.getMonth(); + this.currentLeftYear = today.getFullYear(); + } + this._renderCalendars(); + } + + _updatePresetSelectionVisuals() { + this.container.querySelectorAll('.preset-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + if (!this.tempStartDate && !this.tempEndDate) { + this.container.querySelector('[data-preset="all"]')?.classList.add('active'); + return; + } + + if (this.tempStartDate && this.tempEndDate) { + const activePreset = ['today', 'yesterday', 'last7', 'last14', 'last30', 'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth'].find(preset => { + const { start, end } = this._calculatePreset(preset); + return start && end && + this._datesEqual(this.tempStartDate, start) && + this._datesEqual(this.tempEndDate, end); + }); + + if (activePreset) { + this.container.querySelector(`[data-preset="${activePreset}"]`)?.classList.add('active'); + } + } + } + + _updateTriggerLabel() { + if (!this.startDate && !this.endDate) { + if (this.label) { + this.label.textContent = window.tpT?.('filter_all_time') || 'Semua Waktu'; + this.label.setAttribute('data-tp-i18n', 'filter_all_time'); + } + } else { + if (this.label) { + this.label.removeAttribute('data-tp-i18n'); + const startFormatted = this._formatDateDisplay(this.startDate); + const endFormatted = this._formatDateDisplay(this.endDate); + if (this._datesEqual(this.startDate, this.endDate)) { + this.label.textContent = startFormatted; + } else { + this.label.textContent = `${startFormatted} - ${endFormatted}`; + } + } + } + } + + _bindEvents() { + this.trigger.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggle(); + }); + + this.popover.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + if (this.prevBtn) { + this.prevBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this._navigateMonths(-1); + }); + } + if (this.nextBtn) { + this.nextBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this._navigateMonths(1); + }); + } + + const presetsList = this.container.querySelector('.presets-list'); + if (presetsList) { + presetsList.addEventListener('click', (e) => { + const btn = e.target.closest('.preset-btn'); + if (!btn) return; + e.stopPropagation(); + const preset = btn.getAttribute('data-preset'); + this._handlePresetClick(preset); + }); + } + + if (this.cancelBtn) { + this.cancelBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this._handleCancel(); + }); + } + + if (this.applyBtn) { + this.applyBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this._handleApply(); + }); + } + + if (this.backdrop) { + this.backdrop.addEventListener('click', (e) => { + e.stopPropagation(); + this._handleCancel(); + }); + } + + document.addEventListener('click', () => { + if (this.isOpen) { + this._handleCancel(); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.isOpen) { + this._handleCancel(); + } + }); + } + + _bindDisplayInputs() { + const parseDate = (val) => { + const m = val.trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/); + if (!m) return null; + const d = parseInt(m[1], 10); + const mon = parseInt(m[2], 10) - 1; + const y = parseInt(m[3], 10); + + if (mon < 0 || mon > 11 || d < 1 || d > 31 || y < 1000 || y > 9999) return null; + + const testDate = new Date(y, mon, d); + if (testDate.getFullYear() !== y || testDate.getMonth() !== mon || testDate.getDate() !== d) { + return null; + } + return testDate; + }; + + const setupInput = (input, isStart) => { + if (!input) return; + + input.addEventListener('keydown', (e) => { + const CTRL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter']); + if (!/^\d$/.test(e.key) && e.key !== '/' && e.key !== '-' && e.key !== '.' && !CTRL_KEYS.has(e.key)) { + e.preventDefault(); + } + }); + + input.addEventListener('input', (e) => { + if (e.inputType && e.inputType.startsWith('delete')) { + return; + } + + let val = input.value.replace(/[^\d]/g, ''); + let formatted = val; + if (val.length > 2) { + formatted = val.substring(0, 2) + '/' + val.substring(2); + } + if (val.length > 4) { + formatted = val.substring(0, 2) + '/' + val.substring(2, 4) + '/' + val.substring(4, 8); + } + input.value = formatted; + + const parsed = parseDate(formatted); + if (parsed) { + const today = new Date(); + today.setHours(23, 59, 59, 999); + if (parsed <= today) { + if (isStart) { + this.tempStartDate = parsed; + if (this.tempEndDate && this.tempEndDate < this.tempStartDate) { + this.tempEndDate = new Date(this.tempStartDate); + } + } else { + this.tempEndDate = parsed; + if (this.tempStartDate && this.tempStartDate > this.tempEndDate) { + this.tempStartDate = new Date(this.tempEndDate); + } + } + + this.currentLeftMonth = parsed.getMonth(); + this.currentLeftYear = parsed.getFullYear(); + + const rightMY = this._getRightMonthYear(); + const isEn = this._isEnglish(); + const monthNames = isEn ? + ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] : + ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember']; + if (this.leftMonthName) this.leftMonthName.textContent = monthNames[this.currentLeftMonth]; + if (this.leftYearSelect) this.leftYearSelect.value = this.currentLeftYear; + if (this.rightMonthName) this.rightMonthName.textContent = monthNames[rightMY.month]; + if (this.rightYearSelect) this.rightYearSelect.value = rightMY.year; + if (this.leftDaysContainer) this._populateDaysContainer(this.leftDaysContainer, this.currentLeftMonth, this.currentLeftYear); + if (this.rightDaysContainer) this._populateDaysContainer(this.rightDaysContainer, rightMY.month, rightMY.year); + this._updatePresetSelectionVisuals(); + } + } + }); + + input.addEventListener('blur', () => { + const parsed = parseDate(input.value); + if (!parsed) { + if (isStart) { + input.value = this._formatDateDisplay(this.tempStartDate); + } else { + input.value = this._formatDateDisplay(this.tempEndDate); + } + } + }); + }; + + setupInput(this.startDisplay, true); + setupInput(this.endDisplay, false); + } + + _populateYearSelects() { + if (!this.leftYearSelect || !this.rightYearSelect) return; + + this.leftYearSelect.innerHTML = ''; + this.rightYearSelect.innerHTML = ''; + + const startYear = 2020; + const endYear = new Date().getFullYear() + 4; + + for (let y = startYear; y <= endYear; y++) { + const optLeft = document.createElement('option'); + optLeft.value = y; + optLeft.textContent = y; + this.leftYearSelect.appendChild(optLeft); + + const optRight = document.createElement('option'); + optRight.value = y; + optRight.textContent = y; + this.rightYearSelect.appendChild(optRight); + } + + this.leftYearSelect.addEventListener('change', () => { + this._handleYearChange(parseInt(this.leftYearSelect.value, 10), true); + }); + + this.rightYearSelect.addEventListener('change', () => { + this._handleYearChange(parseInt(this.rightYearSelect.value, 10), false); + }); + } + + _handleYearChange(year, isLeft) { + if (isLeft) { + this.currentLeftYear = year; + } else { + const rightMY = this._getRightMonthYear(); + const rightMonth = rightMY.month; + if (rightMonth === 0) { + this.currentLeftYear = year - 1; + } else { + this.currentLeftYear = year; + } + } + this._renderCalendars(); + } + + toggle() { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + open() { + document.querySelectorAll('.custom-dropdown.active').forEach(dd => { + dd.classList.remove('active'); + dd.querySelector('.custom-dropdown-trigger')?.setAttribute('aria-expanded', 'false'); + }); + + this.isOpen = true; + this.container.classList.add('active'); + this.trigger.setAttribute('aria-expanded', 'true'); + + this.tempStartDate = this.startDate ? new Date(this.startDate) : null; + this.tempEndDate = this.endDate ? new Date(this.endDate) : null; + + const today = new Date(); + if (this.endDate) { + this.currentLeftMonth = this.endDate.getMonth(); + this.currentLeftYear = this.endDate.getFullYear(); + } else { + this.currentLeftMonth = today.getMonth(); + this.currentLeftYear = today.getFullYear(); + } + + this._renderCalendars(); + + if (window.innerWidth <= 768) { + document.body.classList.add('date-picker-open'); + } + } + + close() { + this.isOpen = false; + this.container.classList.remove('active'); + this.trigger.setAttribute('aria-expanded', 'false'); + document.body.classList.remove('date-picker-open'); + } + + _handleCancel() { + this.tempStartDate = this.startDate ? new Date(this.startDate) : null; + this.tempEndDate = this.endDate ? new Date(this.endDate) : null; + this.close(); + } + + _handleApply() { + this.startDate = this.tempStartDate; + this.endDate = this.tempEndDate; + + this._updateTriggerLabel(); + this.close(); + + const startStr = this._getDateString(this.startDate); + const endStr = this._getDateString(this.endDate); + if (this.onChange) { + this.onChange(startStr, endStr); + } + } + + _getDateString(date) { + if (!date) return ''; + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } +} diff --git a/groups/Tapops/js/components/document-card-view.js b/groups/Tapops/js/components/document-card-view.js new file mode 100644 index 0000000..217daa4 --- /dev/null +++ b/groups/Tapops/js/components/document-card-view.js @@ -0,0 +1,73 @@ +import { DirectusClient } from '../services/directus-client.js'; +import { FileDownloader } from '../services/file-downloader.js'; +import { DocumentFormatter } from '../utils/document-formatter.js'; + +export class DocumentCardView { + constructor(item) { + this.item = item; + this.title = item.nama_berkas; + this.tags = DocumentFormatter.parseTags(item.tag); + this.category = DocumentFormatter.categoryFromTags(this.tags); + this.fileType = DocumentFormatter.fileTypeFromName(this.title); + this.assetUrl = DirectusClient.assetUrl(item.file); + this.viewerUrl = DirectusClient.viewerUrl(item); + this.downloadName = DocumentFormatter.downloadFilename(this.title, this.fileType); + this.dateIso = item.tanggal_upload || ''; + this.dateKey = this.dateIso ? this.dateIso.substring(0, 10) : ''; + } + + render() { + const card = document.createElement('article'); + card.className = 'document-card'; + card.dataset.date = this.dateKey; + + const typeUpper = this.fileType.toUpperCase(); + const typeClass = this.fileType === 'pdf' ? 'doc-type-pdf' : 'doc-type-docx'; + const iconClass = this.fileType === 'pdf' ? 'fas fa-file-pdf' : 'fas fa-file-word'; + const formattedDate = DocumentFormatter.formatDate(this.dateIso); + + const safeTitle = DocumentFormatter.escapeHtml(this.title); + const safeViewerUrl = DocumentFormatter.escapeAttr(this.viewerUrl); + const safeAssetUrl = DocumentFormatter.escapeAttr(this.assetUrl); + + // Build tag badges HTML. + const tagBadgesHtml = this.tags.length + ? `
${this.tags.map(t => `${DocumentFormatter.escapeHtml(t)}`).join('')}
` + : ''; + + card.innerHTML = ` +
+

${safeTitle}

+
+ ${typeUpper} + ${formattedDate} +
+ ${tagBadgesHtml} +
+ Buka Berkas + Unduh Berkas +
+ `; + + card.querySelector('.btn-action-outline').addEventListener('click', (e) => { + e.preventDefault(); + FileDownloader.download(this.assetUrl, this.downloadName); + }); + + // Prefetch the asset when the user hovers over the card so the + // browser cache already has the file by the time the viewer loads. + let prefetched = false; + card.addEventListener('mouseenter', () => { + if (prefetched) return; + prefetched = true; + const link = document.createElement('link'); + link.rel = 'prefetch'; + link.as = 'fetch'; + link.crossOrigin = 'anonymous'; + link.href = this.assetUrl; + document.head.appendChild(link); + }, { once: true }); + + return card; + } +} diff --git a/groups/Tapops/js/controllers/accessibility-toggle-controller.js b/groups/Tapops/js/controllers/accessibility-toggle-controller.js new file mode 100644 index 0000000..e2209c3 --- /dev/null +++ b/groups/Tapops/js/controllers/accessibility-toggle-controller.js @@ -0,0 +1,22 @@ +export class AccessibilityToggleController { + constructor({ + triggerSelector = '[data-accessibility-toggle]', + panelId = 'access-panel', + } = {}) { + this.triggers = Array.from(document.querySelectorAll(triggerSelector)); + this.panel = document.getElementById(panelId); + } + + init() { + if (this.triggers.length === 0) return; + this.triggers.forEach(trigger => { + trigger.addEventListener('click', () => this.toggle()); + }); + } + + toggle() { + if (!this.panel) return; + this.panel.classList.toggle('hidden'); + this.panel.classList.toggle('flex'); + } +} diff --git a/groups/Tapops/js/controllers/document-repository.js b/groups/Tapops/js/controllers/document-repository.js new file mode 100644 index 0000000..9d7f851 --- /dev/null +++ b/groups/Tapops/js/controllers/document-repository.js @@ -0,0 +1,466 @@ +import { DirectusClient } from '../services/directus-client.js'; +import { DocumentCardView } from '../components/document-card-view.js'; +import { CustomDropdown } from '../components/custom-dropdown.js'; +import { DateRangePicker } from '../components/date-range-picker.js'; +import { CounterAnimation } from '../components/counter-animation.js'; + +export class DocumentRepository { + static PAGE_SIZE = 6; + + constructor() { + this.gridContainer = document.getElementById('document-grid'); + this.filterWrapper = document.getElementById('tag-filter-wrapper'); + this.mobileFilterWrapper = document.getElementById('mobile-tag-filter-wrapper'); + this.searchInput = document.getElementById('doc-search'); + this.sortFilterWrapper = document.getElementById('sort-filter-wrapper'); + + // Hero stat values (filled from real API data after fetch). + this.statActiveEl = document.getElementById('stat-active-value'); + this.statUpdatesEl = document.getElementById('stat-updates-value'); + + this.cards = []; + this.paginationContainer = null; + this.state = { + filter: 'all', // 'all' or a specific tag string + search: '', + startDate: '', + endDate: '', + sort: 'newest', + page: 1, + }; + } + + + async init() { + if (!this.gridContainer) return; + this._bindControls(); + this._initPagination(); + try { + const items = await DirectusClient.fetchPublished('berkas'); + this._renderCards(items); + this._buildCategoryFilters(items); + this._renderStats(items); + this._applySort(); + this._applyFilters(); + } catch (err) { + console.error(err); + this._renderError(); + this._renderStatsError(); + } + } + + _initPagination() { + const pag = document.createElement('div'); + pag.id = 'doc-pagination'; + pag.className = 'doc-pagination'; + this.gridContainer.insertAdjacentElement('afterend', pag); + this.paginationContainer = pag; + } + + // Days within which an upload counts as a "recent update". + static RECENT_DAYS = 30; + + // Populate the hero stats from real data: + // Dokumen Aktif → number of published documents + // Pembaruan Terbaru → uploads within the last RECENT_DAYS days + _renderStats(items) { + const active = items.length; + + const windowMs = DocumentRepository.RECENT_DAYS * 24 * 60 * 60 * 1000; + const now = Date.now(); + const recent = items.filter(item => { + if (!item.tanggal_upload) return false; + const t = new Date(item.tanggal_upload).getTime(); + return !Number.isNaN(t) && now - t <= windowMs; + }).length; + + CounterAnimation.animateValue(this.statActiveEl, active); + CounterAnimation.animateValue(this.statUpdatesEl, recent); + } + + _renderStatsError() { + if (this.statActiveEl) this.statActiveEl.textContent = '—'; + if (this.statUpdatesEl) this.statUpdatesEl.textContent = '—'; + } + /** + * Fixed category definitions for the sidebar filter. + * Each has a key, an i18n key for bilingual labels, and a fallback label. + */ + static CATEGORIES = [ + { key: 'all', i18n: 'filter_all', label: 'Semua Berkas' }, + { key: 'skripsi', i18n: 'filter_skripsi', label: 'Panduan Skripsi' }, + { key: 'akademik', i18n: 'filter_akademik', label: 'Kalender Akademik' }, + { key: 'praktik', i18n: 'filter_praktik', label: 'Kerja Praktik' }, + { key: 'umum', i18n: 'filter_umum', label: 'Formulir Umum' }, + ]; + + /** + * Build category filter controls: + * – Desktop: sidebar buttons (hidden on mobile via CSS) + * – Mobile: a dropdown (hidden on desktop via CSS) - * Both stay in sync through a shared _setFilter() helper. - */ - _buildCategoryFilters(items) { - if (!this.filterWrapper) return; - - // Clear skeleton placeholder. - this.filterWrapper.innerHTML = ''; - - // ── Desktop buttons ── - const btnGroup = document.createElement('div'); - btnGroup.className = 'filter-btn-group'; - - DocumentRepository.CATEGORIES.forEach(({ key, i18n, label }) => { - const isActive = key === 'all'; - const btn = document.createElement('button'); - btn.className = 'doc-filter-btn' + (isActive ? ' active' : ''); - btn.dataset.filter = key; - btn.textContent = (window.tpT?.(i18n)) || label; - btn.setAttribute('data-tp-i18n', i18n); - - btn.addEventListener('click', () => this._setFilter(key)); - btnGroup.appendChild(btn); - }); - - this.filterWrapper.appendChild(btnGroup); - - // ── Mobile Custom UI Dropdown ── - if (this.mobileFilterWrapper) { - const dropdownOptions = DocumentRepository.CATEGORIES.map(({ key, i18n, label }) => ({ - value: key, - i18nKey: i18n, - text: (window.tpT?.(i18n)) || label - })); - - this._mobileDropdown = new CustomDropdown({ - container: this.mobileFilterWrapper, - options: dropdownOptions, - defaultValue: this.state.filter, - onChange: (value) => this._setFilter(value) - }); - } - - // Store refs for syncing. - this._filterBtnGroup = btnGroup; - - // Re-translate if i18n is active. - window.applyTp?.(this.filterWrapper); - } - - /** - * Shared filter setter — keeps buttons + dropdown in sync. - */ - _setFilter(key) { - this.state.filter = key; - this.state.page = 1; - - // Sync buttons. - if (this._filterBtnGroup) { - this._filterBtnGroup.querySelectorAll('.doc-filter-btn') - .forEach(b => b.classList.toggle('active', b.dataset.filter === key)); - } - - // Sync custom dropdown. - if (this._mobileDropdown && this._mobileDropdown.getValue() !== key) { - this._mobileDropdown.setValue(key); - } - - this._applyFilters(); - } - - _bindControls() { - this.searchInput?.addEventListener('input', (e) => { - this.state.search = e.target.value.toLowerCase().trim(); - this.state.page = 1; - this._applyFilters(); - }); - - // Initialize Custom Date Range Picker - const datePickerGroup = document.getElementById('date-range-control-group'); - if (datePickerGroup) { - this.dateRangePicker = new DateRangePicker({ - containerId: 'date-range-control-group', - onChange: (startDate, endDate) => { - this.state.startDate = startDate; - this.state.endDate = endDate; - this.state.page = 1; - this._applyFilters(); - } - }); - window.applyTp?.(datePickerGroup); - } - - if (this.sortFilterWrapper) { - const sortOptions = [ - { value: 'newest', i18nKey: 'sort_newest', text: (window.tpT?.('sort_newest')) || 'Terbaru' }, - { value: 'oldest', i18nKey: 'sort_oldest', text: (window.tpT?.('sort_oldest')) || 'Terlama' }, - { value: 'az', i18nKey: 'sort_az', text: (window.tpT?.('sort_az')) || 'Nama A-Z' } - ]; - - this._sortDropdown = new CustomDropdown({ - container: this.sortFilterWrapper, - options: sortOptions, - defaultValue: this.state.sort, - prefixIcon: 'fas fa-sort-amount-down', - onChange: (value) => { - this.state.sort = value; - this.state.page = 1; - this._applySort(); - this._applyFilters(); - } - }); - - window.applyTp?.(this.sortFilterWrapper); - } - } - - _renderCards(items) { - this.gridContainer.innerHTML = ''; - - if (items.length === 0) { - this._renderState('repo-empty', 'fas fa-folder-open', '#cbd5e1', '#64748b', - 'state_empty', 'Belum ada dokumen yang tersedia di repositori.'); - return; - } - - this.cards = items.map(item => { - const view = new DocumentCardView(item); - const element = view.render(); - this.gridContainer.appendChild(element); - return { view, element }; - }); - - // Translate the freshly-rendered card labels (Buka/Unduh Berkas). - window.applyTp?.(this.gridContainer); - } - - _renderError() { - this._renderState('repo-error', 'fas fa-exclamation-triangle', '#f87171', '#ef4444', - 'state_error', 'Gagal memuat berkas. Silakan periksa koneksi internet Anda atau coba lagi nanti.'); - } - - // Build the empty/error placeholder via safe DOM APIs (no innerHTML). - // `i18nKey` lets the Tapops i18n layer re-translate the message on toggle. - _renderState(className, iconClass, iconColor, textColor, i18nKey, message) { - this.cards = []; - this.gridContainer.innerHTML = ''; - - const wrapper = document.createElement('div'); - wrapper.className = className; - Object.assign(wrapper.style, { - gridColumn: '1 / -1', - textAlign: 'center', - padding: '60px 20px', - color: textColor, - fontWeight: '700', - fontSize: '1rem', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '15px', - }); - - const icon = document.createElement('i'); - icon.className = iconClass; - Object.assign(icon.style, { fontSize: '3rem', color: iconColor }); - - const span = document.createElement('span'); - if (i18nKey) span.setAttribute('data-tp-i18n', i18nKey); - span.textContent = (window.tpT?.(i18nKey)) || message; - - wrapper.appendChild(icon); - wrapper.appendChild(span); - this.gridContainer.appendChild(wrapper); - } - - _applyFilters() { - const matched = this.cards.filter(({ view }) => this._cardMatches(view)); - const total = matched.length; - - // Handle zero-results state when cards exist but none match the filter. - this._toggleNoResultsState(total === 0 && this.cards.length > 0); - - if (total === 0) { - this.cards.forEach(({ element }) => { element.style.display = 'none'; }); - this._renderPagination(0, 0); - return; - } - - const totalPages = Math.ceil(total / DocumentRepository.PAGE_SIZE); - - // Clamp page to valid range after filter changes. - this.state.page = Math.min(Math.max(this.state.page, 1), totalPages); - - const start = (this.state.page - 1) * DocumentRepository.PAGE_SIZE; - const pageSet = new Set( - matched.slice(start, start + DocumentRepository.PAGE_SIZE).map(c => c.element) - ); - - this.cards.forEach(({ element }) => { - const show = pageSet.has(element); - element.style.display = show ? 'flex' : 'none'; - if (show) { - setTimeout(() => { - element.style.opacity = '1'; - element.style.transform = 'translateY(0)'; - }, 10); - } else { - element.style.opacity = '0'; - element.style.transform = 'translateY(10px)'; - } - }); - - this._renderPagination(totalPages, total); - } - - _toggleNoResultsState(show) { - const existing = this.gridContainer.querySelector('.repo-no-results'); - if (show && !existing) { - const wrapper = document.createElement('div'); - wrapper.className = 'repo-no-results'; - Object.assign(wrapper.style, { - gridColumn: '1 / -1', - textAlign: 'center', - padding: '60px 20px', - color: '#64748b', - fontWeight: '700', - fontSize: '1rem', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '15px', - }); - const icon = document.createElement('i'); - icon.className = 'fas fa-search'; - Object.assign(icon.style, { fontSize: '3rem', color: '#cbd5e1' }); - const span = document.createElement('span'); - span.setAttribute('data-tp-i18n', 'state_no_results'); - span.textContent = (window.tpT?.('state_no_results')) || 'Tidak ada berkas yang cocok dengan filter saat ini.'; - wrapper.appendChild(icon); - wrapper.appendChild(span); - this.gridContainer.appendChild(wrapper); - } else if (!show && existing) { - existing.remove(); - } - } - - _renderPagination(totalPages, total) { - if (!this.paginationContainer) return; - this.paginationContainer.innerHTML = ''; - if (total === 0 || totalPages <= 1) return; - - const current = this.state.page; - const start = (current - 1) * DocumentRepository.PAGE_SIZE + 1; - const end = Math.min(current * DocumentRepository.PAGE_SIZE, total); - - const info = document.createElement('span'); - info.className = 'pagination-info'; - info.textContent = `${start}–${end} dari ${total} berkas`; - this.paginationContainer.appendChild(info); - - const nav = document.createElement('nav'); - nav.className = 'pagination-nav'; - nav.setAttribute('aria-label', 'Navigasi halaman dokumen'); - - const prev = document.createElement('button'); - prev.className = 'pagination-btn' + (current <= 1 ? ' disabled' : ''); - prev.disabled = current <= 1; - prev.setAttribute('aria-label', 'Halaman sebelumnya'); - const prevIcon = document.createElement('i'); - prevIcon.className = 'fas fa-chevron-left'; - prev.appendChild(prevIcon); - prev.addEventListener('click', () => { - if (this.state.page > 1) { this.state.page--; this._applyFilters(); this._scrollToGrid(); } - }); - nav.appendChild(prev); - - this._paginationRange(current, totalPages).forEach(p => { - if (p === '...') { - const el = document.createElement('span'); - el.className = 'pagination-ellipsis'; - el.setAttribute('aria-hidden', 'true'); - el.textContent = '…'; - nav.appendChild(el); - } else { - const btn = document.createElement('button'); - btn.className = 'pagination-btn pagination-number' + (p === current ? ' active' : ''); - btn.textContent = p; - btn.setAttribute('aria-label', `Halaman ${p}`); - if (p === current) btn.setAttribute('aria-current', 'page'); - btn.addEventListener('click', () => { - this.state.page = p; this._applyFilters(); this._scrollToGrid(); - }); - nav.appendChild(btn); - } - }); - - const next = document.createElement('button'); - next.className = 'pagination-btn' + (current >= totalPages ? ' disabled' : ''); - next.disabled = current >= totalPages; - next.setAttribute('aria-label', 'Halaman berikutnya'); - const nextIcon = document.createElement('i'); - nextIcon.className = 'fas fa-chevron-right'; - next.appendChild(nextIcon); - next.addEventListener('click', () => { - if (this.state.page < totalPages) { this.state.page++; this._applyFilters(); this._scrollToGrid(); } - }); - nav.appendChild(next); - - this.paginationContainer.appendChild(nav); - } - - // Returns page numbers and '...' ellipsis placeholders for the given range. - _paginationRange(current, total) { - if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1); - - const pages = [1]; - const lo = Math.max(2, current - 1); - const hi = Math.min(total - 1, current + 1); - - if (lo > 2) pages.push('...'); - for (let i = lo; i <= hi; i++) pages.push(i); - if (hi < total - 1) pages.push('...'); - pages.push(total); - - return pages; - } - - _scrollToGrid() { - document.getElementById('repository')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - - _cardMatches(view) { - const { filter, search } = this.state; - const matchesCategory = filter === 'all' || view.category === filter; - const matchesTitle = view.title.toLowerCase().includes(search); - const matchesTags = search - ? view.tags.some(t => t.toLowerCase().includes(search)) - : false; - const matchesSearch = matchesTitle || matchesTags; - return matchesCategory && matchesSearch && this._dateMatches(view.dateKey); - } - - _dateMatches(dateKey) { - const { startDate, endDate } = this.state; - if (!dateKey) return !startDate && !endDate; - const cardDate = new Date(dateKey); - if (startDate && cardDate < new Date(startDate)) return false; - if (endDate) { - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); - if (cardDate > end) return false; - } - return true; - } - - _applySort() { - const { sort } = this.state; - this.cards.sort((a, b) => { - if (sort === 'newest') return b.view.dateKey.localeCompare(a.view.dateKey); - if (sort === 'oldest') return a.view.dateKey.localeCompare(b.view.dateKey); - if (sort === 'az') return a.view.title.toLowerCase().localeCompare(b.view.title.toLowerCase()); - return 0; - }); - this.cards.forEach(({ element }) => this.gridContainer.appendChild(element)); - } -} - -/* =========================================== - CounterAnimation - =========================================== */ -class CounterAnimation { - static DURATION_MS = 2000; - - init() { - const counters = document.querySelectorAll('.counter-value[data-target]'); - if (counters.length === 0) return; - - if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - this._animate(entry.target); - observer.unobserve(entry.target); - } - }); - }, { threshold: 0.2 }); - counters.forEach(c => observer.observe(c)); - } else { - counters.forEach(c => this._animate(c)); - } - } - - _animate(el) { - const target = Number.parseInt((el.dataset.target || '0').replace(/[^0-9]/g, ''), 10); - if (Number.isNaN(target)) return; - CounterAnimation.animateValue(el, target, el.dataset.suffix || ''); - } - - // Reusable easing tween from 0 → target. Callable for stats whose - // value is only known after an async fetch (no data-target needed). - static animateValue(el, target, suffix = '') { - if (!el || Number.isNaN(target)) return; - const startTime = performance.now(); - - const tick = (now) => { - const progress = Math.min((now - startTime) / CounterAnimation.DURATION_MS, 1); - const eased = 1 - Math.pow(1 - progress, 3); - el.textContent = Math.floor(eased * target) + suffix; - if (progress < 1) requestAnimationFrame(tick); - else el.textContent = target + suffix; - }; - requestAnimationFrame(tick); - } -} - -/* =========================================== - App entry - =========================================== */ -document.addEventListener('DOMContentLoaded', () => { - new DocumentRepository().init(); -}); +import './app.js'; diff --git a/groups/Tapops/js/models/pagination-model.js b/groups/Tapops/js/models/pagination-model.js new file mode 100644 index 0000000..d282e95 --- /dev/null +++ b/groups/Tapops/js/models/pagination-model.js @@ -0,0 +1,16 @@ +export class PaginationModel { + static range(current, total) { + if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1); + + const pages = [1]; + const low = Math.max(2, current - 1); + const high = Math.min(total - 1, current + 1); + + if (low > 2) pages.push('...'); + for (let page = low; page <= high; page += 1) pages.push(page); + if (high < total - 1) pages.push('...'); + pages.push(total); + + return pages; + } +} diff --git a/groups/Tapops/js/services/directus-client.js b/groups/Tapops/js/services/directus-client.js new file mode 100644 index 0000000..96b682a --- /dev/null +++ b/groups/Tapops/js/services/directus-client.js @@ -0,0 +1,31 @@ +import { DocumentFormatter } from '../utils/document-formatter.js'; + +export class DirectusClient { + static API_BASE = 'https://api.ifuntanhub.dev'; + + static itemsUrl(collection) { + return `${this.API_BASE}/items/${collection}?limit=-1`; + } + + static assetUrl(uuid) { + return `${this.API_BASE}/assets/${uuid}`; + } + + static viewerUrl(item) { + const type = DocumentFormatter.fileTypeFromName(item.nama_berkas); + const params = new URLSearchParams({ + id: item.file, + name: item.nama_berkas, + type, + }); + return `viewer.html?${params.toString()}`; + } + + static async fetchPublished(collection) { + const response = await fetch(this.itemsUrl(collection)); + if (!response.ok) throw new Error(`Gagal memuat koleksi ${collection}.`); + const body = await response.json(); + const items = body.data || []; + return items.filter(item => item.status === 'published'); + } +} diff --git a/groups/Tapops/js/services/file-downloader.js b/groups/Tapops/js/services/file-downloader.js new file mode 100644 index 0000000..438a3fb --- /dev/null +++ b/groups/Tapops/js/services/file-downloader.js @@ -0,0 +1,26 @@ +export class FileDownloader { + static async download(url, filename) { + try { + const response = await fetch(url); + if (!response.ok) throw new Error('Gagal mengunduh berkas.'); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + this._click(blobUrl, filename); + URL.revokeObjectURL(blobUrl); + } catch (err) { + console.warn('Unduh via Blob gagal, beralih ke parameter Directus:', err); + const fallbackUrl = url.includes('?') ? `${url}&download` : `${url}?download`; + this._click(fallbackUrl, filename); + } + } + + static _click(href, filename) { + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = href; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } +} diff --git a/groups/Tapops/js/templates/footer-template.js b/groups/Tapops/js/templates/footer-template.js new file mode 100644 index 0000000..206f2fd --- /dev/null +++ b/groups/Tapops/js/templates/footer-template.js @@ -0,0 +1,65 @@ +export function footerTemplate() { + return ` `; +} diff --git a/groups/Tapops/js/templates/main-content-template.js b/groups/Tapops/js/templates/main-content-template.js new file mode 100644 index 0000000..4b8f8b8 --- /dev/null +++ b/groups/Tapops/js/templates/main-content-template.js @@ -0,0 +1,278 @@ +export function mainContentTemplate() { + return `
+ + +
+
+ Digital Administrative Hub +

Administrasi Akademik Digital

+

+ Satu pintu menuju semua dokumen penting. Akses terpusat menuju repositori dokumen akademik Program Studi + Informatika Universitas Tanjungpura untuk mahasiswa dan staf. +

+ +
+ +
+
+ + Dokumen Aktif +
+
+
+ + Pembaruan Terbaru +
+
+
+ + +
+
+ + + + +
+
+
+

Eksplorasi Dokumen Prodi

+

Temukan berkas administrasi dan akademik terbaru Program Studi + Informatika.

+
+ +
+
+
+ + +
+
+ +
+
+ Periode Unggahan + + +
+ +
+
+
+
+ +
+ - +
+ +
+
+
+ +
+
+
+ +
+
+ Mei +
+ + +
+
+
+ Juni +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+ RENTANG WAKTU +
+ + + + + + + + + + +
+
+
+ + +
+
+ +
+ Urutkan +
+
+ +
+ Kategori Berkas +
+
+
+
+
+ +
+
+ + Menghubungkan ke repositori berkas... +
+
+
+
+
+ + +
+
+
+

Tata Cara Pengajuan Berkas

+ +
+ +
+ +
+
+
+
01
+
+
+
+

Unduh Formulir

+

Pilih kategori pengajuan Anda, lalu unduh formulir versi terbaru. Pastikan kode revisi pada formulir sesuai dengan yang tertera di halaman ini.

+
+
+ + +
+
+
+
02
+
+
+
+

Lengkapi Formulir

+

Isi setiap kolom dengan lengkap dan teliti. Sertakan seluruh dokumen pendukung yang diminta sebelum pengiriman.

+
+
+ + +
+
+
+
03
+
+
+

Kirim Berkas

+

Kirimkan formulir beserta dokumen pendukung melalui email resmi atau WhatsApp admin prodi. Berkas yang lengkap akan langsung diproses oleh admin prodi.

+
+
+
+
+
+ + +
+
+
+

Butuh Bantuan Lebih Lanjut?

+

Tim admin kami siap membantu Anda melalui kanal komunikasi resmi di bawah ini.

+
+ +
+
+
+
+

Korespondensi Resmi (Email)

+

Gunakan untuk permohonan form khusus non-publik atau administrasi formal lainnya.

+
    +
  • Respon Maks. 24 Jam
  • +
  • Pelayanan Formal
  • +
+ HUBUNGI ADMIN +
+
+ +
+
+
+

Quick Support (WhatsApp)

+

Konsultasi cepat alur birokrasi atau pelaporan kendala akses pada berkas repositori.

+
    +
  • Respon Instan Jam Kerja
  • +
  • Konsultasi Teknis
  • +
+ CHAT WHATSAPP +
+
+
+
+
+ +
`; +} diff --git a/groups/Tapops/js/templates/navigation-template.js b/groups/Tapops/js/templates/navigation-template.js new file mode 100644 index 0000000..af23dbd --- /dev/null +++ b/groups/Tapops/js/templates/navigation-template.js @@ -0,0 +1,843 @@ +export function navigationTemplate() { + return ` + + + + `; +} diff --git a/groups/Tapops/js/templates/page-template.js b/groups/Tapops/js/templates/page-template.js new file mode 100644 index 0000000..95d6ddf --- /dev/null +++ b/groups/Tapops/js/templates/page-template.js @@ -0,0 +1,7 @@ +import { navigationTemplate } from './navigation-template.js'; +import { mainContentTemplate } from './main-content-template.js'; +import { footerTemplate } from './footer-template.js'; + +export function pageTemplate() { + return [navigationTemplate(), mainContentTemplate(), footerTemplate()].join(''); +} diff --git a/groups/Tapops/js/utils/date-range-calculator.js b/groups/Tapops/js/utils/date-range-calculator.js new file mode 100644 index 0000000..7e532bd --- /dev/null +++ b/groups/Tapops/js/utils/date-range-calculator.js @@ -0,0 +1,112 @@ +export class DateRangeCalculator { + static clearTime(date) { + if (!date) return null; + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; + } + + static datesEqual(left, right) { + if (!left || !right) return false; + return left.getFullYear() === right.getFullYear() + && left.getMonth() === right.getMonth() + && left.getDate() === right.getDate(); + } + + static calculatePreset(presetName, todayInput = new Date()) { + const today = this.clearTime(todayInput); + let start = null; + let end = null; + + switch (presetName) { + case 'today': + start = new Date(today); + end = new Date(today); + break; + case 'yesterday': + start = new Date(today); + start.setDate(today.getDate() - 1); + end = new Date(start); + break; + case 'last7': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 6); + break; + case 'last14': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 13); + break; + case 'last30': + end = new Date(today); + start = new Date(today); + start.setDate(today.getDate() - 29); + break; + case 'thisWeek': { + end = new Date(today); + start = new Date(today); + const day = today.getDay(); + const diff = today.getDate() - day + (day === 0 ? -6 : 1); + start.setDate(diff); + break; + } + case 'lastWeek': { + const temp = new Date(today); + const day = temp.getDay(); + const diff = temp.getDate() - day + (day === 0 ? -6 : 1); + start = new Date(temp); + start.setDate(diff - 7); + end = new Date(temp); + end.setDate(diff - 1); + break; + } + case 'thisMonth': + start = new Date(today.getFullYear(), today.getMonth(), 1); + end = new Date(today); + break; + case 'lastMonth': + start = new Date(today.getFullYear(), today.getMonth() - 1, 1); + end = new Date(today.getFullYear(), today.getMonth(), 0); + break; + case 'all': + default: + start = null; + end = null; + break; + } + + return { start, end }; + } + + static formatDisplay(date) { + if (!date) return ''; + const d = new Date(date); + const day = String(d.getDate()).padStart(2, '0'); + const month = String(d.getMonth() + 1).padStart(2, '0'); + return `${day}/${month}/${d.getFullYear()}`; + } + + static parseDisplayDate(value) { + const match = String(value || '').trim().match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{4})$/); + if (!match) return null; + + const day = Number.parseInt(match[1], 10); + const month = Number.parseInt(match[2], 10) - 1; + const year = Number.parseInt(match[3], 10); + if (month < 0 || month > 11 || day < 1 || day > 31 || year < 1000 || year > 9999) return null; + + const date = new Date(year, month, day); + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null; + return date; + } + + static toDateKey(date) { + if (!date) return ''; + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } +} diff --git a/groups/Tapops/js/utils/document-formatter.js b/groups/Tapops/js/utils/document-formatter.js new file mode 100644 index 0000000..6e6702a --- /dev/null +++ b/groups/Tapops/js/utils/document-formatter.js @@ -0,0 +1,71 @@ +export class DocumentFormatter { + static MONTHS_ID = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; + + static CATEGORY_RULES = [ + { key: 'skripsi', match: ['skripsi', 'tugas akhir', 'ta'] }, + { key: 'akademik', match: ['akademik', 'kalender', 'kaldik'] }, + { key: 'praktik', match: ['praktik', 'praktek', 'kerja praktik', 'kp'] }, + ]; + + static formatDate(iso) { + if (!iso) return ''; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return ''; + const day = String(date.getDate()).padStart(2, '0'); + const month = this.MONTHS_ID[date.getMonth()]; + return `${day} ${month} ${date.getFullYear()}`; + } + + static categoryFromTags(tags) { + if (!tags || tags.length === 0) return 'umum'; + const lower = tags.map(tag => tag.toLowerCase()); + for (const rule of this.CATEGORY_RULES) { + if (rule.match.some(match => lower.includes(match))) return rule.key; + } + return 'umum'; + } + + static fileTypeFromName(name) { + const lower = (name || '').toLowerCase(); + if (lower.endsWith('.docx') || lower.endsWith('.doc')) return 'docx'; + return 'pdf'; + } + + static downloadFilename(name, type) { + const ext = `.${type}`; + return name.toLowerCase().endsWith(ext) ? name : `${name}${ext}`; + } + + static escapeHtml(str) { + if (str == null) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + static escapeAttr(str) { + return this.escapeHtml(str); + } + + static parseTags(raw) { + if (!raw) return []; + const str = String(raw).trim(); + if (!str) return []; + + if (str.startsWith('[')) { + try { + const arr = JSON.parse(str); + if (Array.isArray(arr)) { + return arr.map(tag => String(tag).trim()).filter(Boolean); + } + } catch { + // Fall through to comma-separated parsing. + } + } + + return str.split(',').map(tag => tag.trim()).filter(Boolean); + } +} diff --git a/groups/Tapops/js/viewer.js b/groups/Tapops/js/viewer.js index 39bb332..f4c57c5 100644 --- a/groups/Tapops/js/viewer.js +++ b/groups/Tapops/js/viewer.js @@ -1,220 +1 @@ -/** - * Viewer page — renders a single Directus asset in-browser. - * PDF → native