diff --git a/app/Http/Controllers/Files/AreaFilesController.php b/app/Http/Controllers/Files/AreaFilesController.php index ec398a159..ce3c20646 100644 --- a/app/Http/Controllers/Files/AreaFilesController.php +++ b/app/Http/Controllers/Files/AreaFilesController.php @@ -16,8 +16,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Facades\Log; -use Mostafaznv\PdfOptimizer\Laravel\Facade\PdfOptimizer; +// Compression is handled client-side before upload — no server-side optimizer needed. class AreaFilesController extends Controller @@ -29,8 +28,11 @@ public function store(Request $request) { $validated = $request->validate( [ - 'outline_id' => 'nullable|exists:parameter_outlines,parameter_outline_id', - 'document' => 'required|file|mimes:pdf' + 'outline_id' => 'required|integer|exists:parameter_outlines,parameter_outline_id', + 'document' => 'required|file|mimes:pdf|max:10240', + 'program_id' => 'required|integer|exists:programs,program_id', + 'level_id' => 'required|integer|exists:accreditation_levels,accreditation_level_id', + 'area_id' => 'required|integer|exists:areas,area_id', ], [ 'outline_id.exists' => 'The selected outline does not exist.', @@ -88,57 +90,8 @@ public function store(Request $request) $category_name = Str::slug($categoryName, '_'); $filePath = "documents/{$degree_type}_{$program_name}/{$level}/{$area_name}/{$parameter_name}/{$category_name}"; - // Ensure temp directory exists - if (!Storage::disk('public')->exists('temp')) { - Storage::disk('public')->makeDirectory('temp'); - } - - // Store original file temporarily - $tempFileName = 'temp_' . Str::uuid() . '.pdf'; - $tempFilePath = "temp/{$tempFileName}"; - Storage::disk('public')->putFileAs('temp', $file, $tempFileName); - - Log::info('Temp file created: ' . $tempFilePath); - Log::info('Temp file size: ' . Storage::disk('public')->size($tempFilePath) . ' bytes'); - - // Optimize PDF - try { - Log::info('Starting PDF optimization...'); - $customTempPath = str_replace('/', '\\', storage_path('app/public/temp')); - putenv('TMP=' . $customTempPath); - putenv('TEMP=' . $customTempPath); - - Log::info('Using custom TEMP/TMP path: ' . $customTempPath); - - $result = PdfOptimizer::fromDisk('public') - ->open($tempFilePath) - ->toDisk('public') - ->optimize("{$filePath}/{$fileName}"); - - Log::info('Optimization status: ' . ($result->status ? 'SUCCESS' : 'FAILED')); - Log::info('Optimization message: ' . $result->message); - - if (Storage::disk('public')->exists("{$filePath}/{$fileName}")) { - Log::info('Optimized file created successfully'); - Log::info('Optimized file size: ' . Storage::disk('public')->size("{$filePath}/{$fileName}") . ' bytes'); - } else { - Log::error('Optimized file NOT found!'); - } - - // Delete temporary file - Storage::disk('public')->delete($tempFilePath); - Log::info('Temp file deleted'); - - if (!$result->status) { - Storage::disk('public')->putFileAs($filePath, $file, $fileName); - Log::warning('PDF optimization failed, using original file: ' . $result->message); - } - } catch (\Exception $e) { - Storage::disk('public')->delete($tempFilePath); - Storage::disk('public')->putFileAs($filePath, $file, $fileName); - Log::error('PDF optimization error: ' . $e->getMessage()); - Log::error('Stack trace: ' . $e->getTraceAsString()); - } + // PDF is already compressed by the client before upload — store directly. + Storage::disk('public')->putFileAs($filePath, $file, $fileName); $areaFile = $parameterOutlines->AreaFiles()->create([ 'file_name' => $fileName, @@ -169,7 +122,7 @@ public function download(Request $request) $areaFile = $parameterOutlines->AreaFiles; if ($areaFile && Storage::disk('public')->exists($areaFile->file_path)) { - return Storage::disk('public')->download($areaFile->file_path, $areaFile->file_name); + return response()->download(storage_path("app/public/" . $areaFile->file_path), $areaFile->file_name); } else { return redirect()->back() ->with('type', 'error') diff --git a/app/Http/Controllers/Files/AreaFormsController.php b/app/Http/Controllers/Files/AreaFormsController.php index 9feee10dd..2990e2238 100644 --- a/app/Http/Controllers/Files/AreaFormsController.php +++ b/app/Http/Controllers/Files/AreaFormsController.php @@ -27,7 +27,10 @@ public function store(Request $request): RedirectResponse $validated = $request->validate( [ 'area_form_category_id' => 'required|integer|exists:area_form_categories,area_form_category_id', - 'document' => 'nullable|file|mimes:pdf', + 'document' => 'nullable|file|mimes:pdf|max:10240', + 'program_id' => 'required|integer|exists:programs,program_id', + 'level_id' => 'required|integer|exists:accreditation_levels,accreditation_level_id', + 'area_id' => 'required|integer|exists:areas,area_id', ], [ 'area_form_category_id.required' => 'Form category ID is required.', diff --git a/app/Http/Controllers/TestingShits.php b/app/Http/Controllers/TestingShits.php deleted file mode 100644 index 949c63783..000000000 --- a/app/Http/Controllers/TestingShits.php +++ /dev/null @@ -1,64 +0,0 @@ -headers->set('X-Frame-Options', 'SAMEORIGIN'); + + // Prevent MIME-type sniffing + $response->headers->set('X-Content-Type-Options', 'nosniff'); + + // Basic XSS protection (though modern browsers handle this better) + $response->headers->set('X-XSS-Protection', '1; mode=block'); + + // Referrer Policy + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Content Security Policy (Liberalized for Development) + // We allow all origins (*) to ensure external resources like Facebook, YouTube, and Google Maps are not blocked. + $csp = "default-src 'self' * 'unsafe-inline' 'unsafe-eval' data: blob:; "; + $csp .= "frame-src 'self' *; "; + $csp .= "frame-ancestors 'self' *; "; + $csp .= "object-src 'none';"; + + $response->headers->set('Content-Security-Policy', $csp); + + return $response; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 001401362..9a55a25e9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -14,6 +14,8 @@ use App\Http\Middleware\UserProgramPrivileges; use App\Http\Middleware\UserAreaPrivileges; +use App\Http\Middleware\SecurityHeaders; + return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', @@ -25,6 +27,7 @@ $middleware->web(append: [ HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, + // SecurityHeaders::class, ]); /* $middleware->api(prepend: [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, diff --git a/composer.json b/composer.json index e24af291d..c948bb617 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,6 @@ "laravel/reverb": "^1.0", "laravel/tinker": "^2.10.1", "league/csv": "^9.27", - "mostafaznv/pdf-optimizer": "^1.2", "tightenco/ziggy": "^2.4" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 6c3921a99..24391352b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "512f2965bfdce6e47d5f06d5776ab40f", + "content-hash": "2c2d5b9ea275513741f7f23fe3625a48", "packages": [ { "name": "brick/math", @@ -2539,77 +2539,6 @@ ], "time": "2025-03-24T10:02:05+00:00" }, - { - "name": "mostafaznv/pdf-optimizer", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/mostafaznv/pdf-optimizer.git", - "reference": "2fb124ca06f57b762ed3b03b4cad9c850cba5a06" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mostafaznv/pdf-optimizer/zipball/2fb124ca06f57b762ed3b03b4cad9c850cba5a06", - "reference": "2fb124ca06f57b762ed3b03b4cad9c850cba5a06", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": "^8.2", - "psr/log": "^3.0", - "symfony/process": "^6.2|^7.0" - }, - "require-dev": { - "league/flysystem-aws-s3-v3": "^3.0", - "orchestra/testbench": "^v8.19.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.7", - "phpunit/phpunit": "^10.0|^11.5.3" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Mostafaznv\\PdfOptimizer\\PdfOptimizerServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/Helpers/Utils.php" - ], - "psr-4": { - "Mostafaznv\\PdfOptimizer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "mostafaznv", - "email": "mostafa.zeinivand@gmail.com" - } - ], - "description": "PDF optimization tool for PHP and Laravel applications", - "keywords": [ - "compress-pdf", - "ghostscript", - "laravel", - "mostafaznv", - "pdf", - "pdf-compressor", - "pdf-optimizer", - "php", - "tiny-pdf" - ], - "support": { - "docs": "https://github.com/mostafaznv/pdf-optimizer/blob/master/README.md", - "issues": "https://github.com/mostafaznv/pdf-optimizer/issues", - "source": "https://github.com/mostafaznv/pdf-optimizer" - }, - "time": "2025-09-24T21:51:40+00:00" - }, { "name": "nesbot/carbon", "version": "3.11.0", diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 000000000..b01aaedfe --- /dev/null +++ b/config/cors.php @@ -0,0 +1,31 @@ + ['*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], // ← FULLY OPEN + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, // ← changed to false (required when using *) +]; \ No newline at end of file diff --git a/config/pdf-optimizer.php b/config/pdf-optimizer.php deleted file mode 100644 index 6de8e5495..000000000 --- a/config/pdf-optimizer.php +++ /dev/null @@ -1,16 +0,0 @@ - [ - 'binary' => env('PDF_OPTIMIZER_BIN_PATH', 'C:\\Program Files\\gs\\gs10.06.0\\bin\\gswin64c.exe'), - ], - 'options' => [ - '-dBATCH', - '-dNOPAUSE', - '-q', - '-sDEVICE=pdfwrite', - '-dPDFSETTINGS=/screen', - '-sTEMP=C:\\Users\\LENOVO\\Desktop\\Capstone\\PUPCON\\storage\\app\\public\\temp', - //'-sTEMP=C:\\Users\\LENOVO\\Desktop\\Capstone\\PUPCON\\storage\\app\\public\\temp', - ], -]; diff --git a/knip.json b/knip.json new file mode 100644 index 000000000..e47f974c9 --- /dev/null +++ b/knip.json @@ -0,0 +1,20 @@ +{ + "entry": [ + "resources/js/app.tsx", + "resources/js/ssr.jsx", + "resources/scss/app.scss", + "resources/js/components/ui/**/*.{ts,tsx}", + "resources/js/echo.js" + ], + "project": ["resources/js/**/*.{js,jsx,ts,tsx}", "resources/scss/**/*.scss", "resources/css/**/*.css"], + "ignoreDependencies": [ + "concurrently", + "@inertiajs/core", + "zod", + "date-fns" + ], + "rules": { + "exports": "off", + "types": "off" + } +} diff --git a/package.json b/package.json index 6b17b606d..e47908d0f 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,7 @@ "typescript-eslint": "^8.23.0" }, "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^2.2.0", - "@hookform/resolvers": "^5.2.2", "@inertiajs/core": "^2.2.19", "@inertiajs/react": "^2.2.19", "@radix-ui/react-accordion": "^1.2.3", @@ -53,7 +48,6 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", - "@tabler/icons-react": "^3.34.0", "@tailwindcss/vite": "^4.0.6", "@tanstack/react-table": "^8.21.3", "@types/react-dom": "^19.0.2", @@ -70,6 +64,8 @@ "lucide-react": "^0.475.0", "next-themes": "^0.4.6", "nprogress": "^0.2.0", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^5.6.205", "react": "^19.1.0", "react-day-picker": "^9.11.2", "react-dom": "^19.1.0", @@ -77,7 +73,6 @@ "react-loading-skeleton": "^3.5.0", "react-pdf": "^10.2.0", "recharts": "^2.15.2", - "shepherd.js": "^14.5.1", "sonner": "^2.0.7", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", @@ -85,6 +80,7 @@ "typescript": "^5.7.2", "vaul": "^1.1.2", "vite": "^6.0", + "ziggy-js": "^2.6.2", "zod": "^3.25.76" }, "optionalDependencies": { diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 12fefc321..48cfb5698 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -11,13 +11,19 @@ declare global { const route: typeof routeFn; } +import ErrorBoundary from './components/error-boundary'; + createInertiaApp({ title: (title) => `${title}`, resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')), setup({ el, App, props }) { const root = createRoot(el); - root.render(); + root.render( + + + + ); }, progress: { color: '#daa520', // Your brand color diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx deleted file mode 100644 index 4c3510ddb..000000000 --- a/resources/js/components/app-header.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; -import { Icon } from '@/components/icon'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { UserMenuContent } from '@/components/user-menu-content'; -import { useInitials } from '@/hooks/use-initials'; -import { cn } from '@/lib/utils'; -import { type BreadcrumbItem, type NavItem, type SharedData } from '@/types'; -import { Link, usePage } from '@inertiajs/react'; -import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-react'; -import AppLogo from './app-logo'; -import AppLogoIcon from './app-logo-icon'; - -const mainNavItems: NavItem[] = [ - { - title: 'Analytics', - url: '/dashboard', - icon: LayoutGrid, - }, -]; - -const rightNavItems: NavItem[] = [ - { - title: 'Repository', - url: 'https://github.com/laravel/react-starter-kit', - icon: Folder, - }, - { - title: 'Documentation', - url: 'https://laravel.com/docs/starter-kits', - icon: BookOpen, - }, -]; - -const activeItemStyles = 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100'; - -interface AppHeaderProps { - breadcrumbs?: BreadcrumbItem[]; -} - -export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) { - const page = usePage(); - const { auth } = page.props; - const getInitials = useInitials(); - return ( - <> -
-
- {/* Mobile Menu */} -
- - - - - - Navigation Menu - - - -
-
-
- {mainNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
- -
- {rightNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
-
-
-
-
-
- - - - - - {/* Desktop Navigation */} -
- - - {mainNavItems.map((item, index) => ( - - - {item.icon && } - {item.title} - - {page.url === item.url && ( -
- )} -
- ))} -
-
-
- -
-
- -
- {rightNavItems.map((item) => ( - - - - - {item.title} - {item.icon && } - - - -

{item.title}

-
-
-
- ))} -
-
- - - - - - - - -
-
-
- {breadcrumbs.length > 1 && ( -
-
- -
-
- )} - - ); -} diff --git a/resources/js/components/app-logo-icon.tsx b/resources/js/components/app-logo-icon.tsx deleted file mode 100644 index 9bd62ad84..000000000 --- a/resources/js/components/app-logo-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { SVGAttributes } from 'react'; - -export default function AppLogoIcon(props: SVGAttributes) { - return ( - - - - ); -} diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx deleted file mode 100644 index a03784cc4..000000000 --- a/resources/js/components/app-logo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import AppLogoIcon from './app-logo-icon'; - -export default function AppLogo() { - return ( - <> -
- -
-
- Laravel Starter Kit -
- - ); -} diff --git a/resources/js/components/appearance-dropdown.tsx b/resources/js/components/appearance-dropdown.tsx deleted file mode 100644 index 89a458620..000000000 --- a/resources/js/components/appearance-dropdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { useAppearance } from '@/hooks/use-appearance'; -import { Monitor, Moon, Sun } from 'lucide-react'; -import { HTMLAttributes } from 'react'; - -export default function AppearanceToggleDropdown({ className = '', ...props }: HTMLAttributes) { - const { appearance, updateAppearance } = useAppearance(); - - const getCurrentIcon = () => { - switch (appearance) { - case 'dark': - return ; - case 'light': - return ; - default: - return ; - } - }; - - return ( -
- - - - - - updateAppearance('light')}> - - - Light - - - updateAppearance('dark')}> - - - Dark - - - updateAppearance('system')}> - - - System - - - - -
- ); -} diff --git a/resources/js/components/charts/data-table-columns/requests.tsx b/resources/js/components/charts/data-table-columns/requests.tsx index a67dc55ea..d988472b0 100644 --- a/resources/js/components/charts/data-table-columns/requests.tsx +++ b/resources/js/components/charts/data-table-columns/requests.tsx @@ -39,6 +39,40 @@ const getAcronym = (text: string) => { .toUpperCase(); }; +const OutlineCell = ({ row }: { row: any }) => { + const [dialogOpen, setDialogOpen] = useState(false); + const origpath = row.original.file_path ?? ''; + + const segments = origpath.split('/'); + const hasNoCategory = segments.includes('no_category'); + + const prefix = hasNoCategory + ? '' + : (origpath + .split('/') + .pop() + ?.match(/^[A-Za-z0-9](?:\.\d+)+\./)?.[0] ?? ''); + + return ( + <> +
+
setDialogOpen(true)} + className="max-w-sm cursor-pointer truncate text-foreground underline transition-colors hover:text-primary" + > + {prefix + ' ' + row.getValue('outline')} +
+
+ + + ); +}; + export function getRequestsColumns({ resolveDialog }: DocumentRecordProps): ColumnDef[] { return [ { @@ -77,7 +111,6 @@ export function getRequestsColumns({ resolveDialog }: DocumentRecordProps): Colu ), cell: ({ row }) => { - usePoll(5000); const filePath = row.original.file_path; const pathSegments = filePath.split('/').filter(Boolean); let rawSegment = ''; @@ -89,23 +122,17 @@ export function getRequestsColumns({ resolveDialog }: DocumentRecordProps): Colu rawSegment = pathSegments[pathSegments.length - 2]; } - const cleanedPath = rawSegment.replace(/_/g, ' '); - const finalPathName = - cleanedPath.charAt(0).toUpperCase() + cleanedPath.slice(1); - const fileType = row.getValue('file_type') as string; const cleanedData = fileType.replace(/-/g, ' '); let subjectPart = ''; let segmentPart = ''; - let levelPart = ''; const regex = /(.*?)\s(Level\s\d)\s(.*)/i; const match = cleanedData.match(regex); if (match) { subjectPart = match[1].trim(); - levelPart = match[2]; const rawSegmentDetail = match[3]; if (rawSegmentDetail.includes('Area')) { @@ -125,7 +152,6 @@ export function getRequestsColumns({ resolveDialog }: DocumentRecordProps): Colu } else { subjectPart = cleanedData; segmentPart = 'N/A'; - levelPart = ''; } return ( @@ -157,39 +183,7 @@ export function getRequestsColumns({ resolveDialog }: DocumentRecordProps): Colu ), - cell: ({ row }) => { - const [dialogOpen, setDialogOpen] = useState(false); - const origpath = row.original.file_path ?? ''; - - const segments = origpath.split('/'); - const hasNoCategory = segments.includes('no_category'); - - const prefix = hasNoCategory - ? '' - : (origpath - .split('/') - .pop() - ?.match(/^[A-Za-z0-9](?:\.\d+)+\./)?.[0] ?? ''); - - return ( - <> -
-
setDialogOpen(true)} - className="max-w-sm cursor-pointer truncate text-foreground underline transition-colors hover:text-primary" - > - {prefix + ' ' + row.getValue('outline')} -
-
- - - ); - }, + cell: ({ row }) => , enableGlobalFilter: true, }, { diff --git a/resources/js/components/charts/data-table-columns/users.tsx b/resources/js/components/charts/data-table-columns/users.tsx index 718575bc8..8c08e1677 100644 --- a/resources/js/components/charts/data-table-columns/users.tsx +++ b/resources/js/components/charts/data-table-columns/users.tsx @@ -129,6 +129,12 @@ export function getUserColumns({ programRoles, roles, resolveDialog }: UserRecor ); }, }, + { + accessorKey: 'is_active', + header: () => null, + cell: () => null, + enableHiding: true, + }, { id: 'actions', header: () =>
Actions
, diff --git a/resources/js/components/column-header.tsx b/resources/js/components/column-header.tsx deleted file mode 100644 index b70d32946..000000000 --- a/resources/js/components/column-header.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Column } from "@tanstack/react-table" -import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" - -interface DataTableColumnHeaderProps - extends React.HTMLAttributes { - column: Column - title: string -} - -export function DataTableColumnHeader({ - column, - title, - className, -}: DataTableColumnHeaderProps) { - if (!column.getCanSort()) { - return
{title}
- } - - return ( -
- - - - - - column.toggleSorting(false)}> - - Asc - - column.toggleSorting(true)}> - - Desc - - - column.toggleVisibility(false)}> - - Hide - - - -
- ) -} diff --git a/resources/js/components/column-toggle.tsx b/resources/js/components/column-toggle.tsx deleted file mode 100644 index b6b168474..000000000 --- a/resources/js/components/column-toggle.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client" - -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu" -import { Table } from "@tanstack/react-table" -import { ColumnsIcon } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" - -export function DataTableViewOptions({ - table, -}: { - table: Table -}) { - return ( - - - - - - Toggle columns - - {table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && column.getCanHide() - ) - .map((column) => { - return ( - column.toggleVisibility(!!value)} - > - {typeof column.columnDef.header === "string" - ? column.columnDef.header - : column.id.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())} - - ) - })} - - - ) -} diff --git a/resources/js/components/content/facilities-content.tsx b/resources/js/components/content/facilities-content.tsx index a5d4d115a..6c364ccee 100644 --- a/resources/js/components/content/facilities-content.tsx +++ b/resources/js/components/content/facilities-content.tsx @@ -263,7 +263,6 @@ const FacilitiesSection: React.FC = ({ ...props }: FacilitiesProps) => {

Select a Facility

{facilityList?.map((facility, index) => ( - // console.log('Rendering facility:', facility),
{ diff --git a/resources/js/components/content/history/directors.tsx b/resources/js/components/content/history/directors.tsx index c67f77223..ac0a05931 100644 --- a/resources/js/components/content/history/directors.tsx +++ b/resources/js/components/content/history/directors.tsx @@ -106,7 +106,6 @@ export function DirectorsSection({ ...props }: DirectorsProps) { updatedList = [...current, directorForLocalState]; } - console.log(updatedList); onUpdateDirectors(directorForLocalState, director); return updatedList; }); diff --git a/resources/js/components/content/local-task-force-content.tsx b/resources/js/components/content/local-task-force-content.tsx index d7aba491f..61963e712 100644 --- a/resources/js/components/content/local-task-force-content.tsx +++ b/resources/js/components/content/local-task-force-content.tsx @@ -111,7 +111,6 @@ const LocalTaskForceSection: React.FC = ({ ...props }: LocalTaskForceSectionProp }; const handleSubmit = () => { - console.log('Submitting Local Task Force Data:', data); post(route('content.local_task_force.update'), { preserveScroll: true, preserveState: true, diff --git a/resources/js/components/content/program/program-section.tsx b/resources/js/components/content/program/program-section.tsx index b93c49f31..96dfd59e8 100644 --- a/resources/js/components/content/program/program-section.tsx +++ b/resources/js/components/content/program/program-section.tsx @@ -343,7 +343,6 @@ export default function ProgramSection({ program, overviewRef, objectivesRef, ga }; const onSave = () => { - console.log('Saving program content...', data); post( route('manage.program.update.content', { program_id: program.program_id, diff --git a/resources/js/components/content/vmgo-content.tsx b/resources/js/components/content/vmgo-content.tsx index 045dec28c..f65cce5cf 100644 --- a/resources/js/components/content/vmgo-content.tsx +++ b/resources/js/components/content/vmgo-content.tsx @@ -85,7 +85,6 @@ const VmgoContentSection: React.FC = ({ ...props }: VmgoContentSectionProps) => }; const handleSave = () => { - console.log('Submitting VMGO Data:', data.pillars); post(route('content.vmgo.update'), { preserveScroll: true, preserveState: true, diff --git a/resources/js/components/dashboard/areas/parameter-accordion.tsx b/resources/js/components/dashboard/areas/parameter-accordion.tsx index 764dd43fb..f15d89f09 100644 --- a/resources/js/components/dashboard/areas/parameter-accordion.tsx +++ b/resources/js/components/dashboard/areas/parameter-accordion.tsx @@ -3,7 +3,7 @@ import { buildOutlineTree, RecursiveOutlineForm } from '@/components/recursive-o import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Button } from '@/components/ui/button'; import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'; -import { Area, AreaFormCategory, type AreaParameters, type ParameterOutlineCategory, ParameterOutlines, Program } from '@/types'; +import { Area, type AreaParameters, type ParameterOutlineCategory, ParameterOutlines, Program } from '@/types'; import { usePage } from '@inertiajs/react'; import { FolderPlus, Pencil, PlusCircle, Trash2 } from 'lucide-react'; @@ -22,7 +22,7 @@ interface BenchDialogParams { interface ParamDialogParams { type: 'add' | 'import' | 'edit' | 'delete'; - parameter: AreaParameters; + parameter?: AreaParameters; } interface ParameterAccordionProps { @@ -44,91 +44,101 @@ export default function ParameterAccordion({ resolveBenchDialog, resolveParamDialog, }: ParameterAccordionProps) { - const { auth } = usePage().props; - const role = auth.user.roles.role_name; + const { auth } = usePage().props; + const role = auth?.user?.roles?.role_name; + + const activeLevel = Array.isArray(program.levels) ? program.levels[0] : program.levels; const canShowActions = (role === 'Admin' || role === 'Coordinator') && - program.levels[0]?.is_active && - program.levels[0]?.remarks === 'Ongoing Survey' && + activeLevel?.is_active && + activeLevel?.remarks === 'Ongoing Survey' && !area.archive === true; return ( <> - {areaParameters?.length > 0 ? ( + {areaParameters && areaParameters.length > 0 ? ( areaParameters?.map((parameter) => ( - -
-

- {!parameter.parameter_name?.trim() - ? `${parameter.parameter_description}` - : `Parameter ${parameter.parameter_name.toUpperCase()[0]}` - } -

-

- {parameter.parameter_name?.trim() ? parameter.parameter_description : ''} -

-
+
+ +
+

+ {parameter.parameter_name?.trim() + ? `Parameter ${parameter.parameter_name}` + : parameter.parameter_description + } +

+

+ {parameter.parameter_name?.trim() ? parameter.parameter_description : ''} +

+
+
+ {canShowActions && ( -
+
)} - - - {parameter.parameter_outlines?.length > 0 ? ( - parameterOutlineCategories.map((category) => { - const outlines = - parameter.parameter_outlines?.filter( - (outline) => - outline.parameter_outline_category_id === - category.parameter_outline_category_id, - ) || []; - if (outlines.length === 0) return null; +
+ + {parameter.parameter_outlines && parameter.parameter_outlines.length > 0 ? ( + parameterOutlineCategories?.map((category) => { + // Optimized filtering without in-place mutation + const filteredOutlines = (parameter.parameter_outlines || []) + .filter(o => o.parameter_outline_category_id === category.parameter_outline_category_id) + .map(o => ({ + ...o, + initial: category.category_name === 'No Category' + ? (parameter.parameter_name?.trim() ? parameter.parameter_name.toUpperCase().match(alphaRegex)?.[0] || '' : '') + : category.category_name.match(alphaRegex)?.[0] || '' + })); - outlines.map((outline) => { - outline.initial = - category.category_name === 'No Category' - ? parameter.parameter_name === '' - ? '' - : parameter.parameter_name.toUpperCase().match(alphaRegex) - : category.category_name.match(alphaRegex); - }); + if (filteredOutlines.length === 0) return null; - const sortedOutlines = buildOutlineTree({ outlines }); + const sortedOutlines = buildOutlineTree({ outlines: filteredOutlines }); return (
-

- {category.category_name === 'No Category' ? '' : category.category_name} +

+ {category.category_name === 'No Category' ? 'General Benchmarks' : category.category_name}

@@ -136,31 +146,33 @@ export default function ParameterAccordion({ ); }) ) : ( -

- No outlines available for this parameter. -

+
+ No benchmarks available for this parameter. +
)} {canShowActions && ( - +
+ +
)} )) ) : ( - + - + Content Not Available No available parameters in this area. diff --git a/resources/js/components/data-table-components/data-table-header.tsx b/resources/js/components/data-table-components/data-table-header.tsx deleted file mode 100644 index 13bf2be22..000000000 --- a/resources/js/components/data-table-components/data-table-header.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { type Column } from "@tanstack/react-table" -import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { cn } from "@/lib/utils" - -interface DataTableColumnHeaderProps - extends React.HTMLAttributes { - column: Column - title: string -} - -export function DataTableColumnHeader({ - column, - title, - className, -}: DataTableColumnHeaderProps) { - if (!column.getCanSort()) { - return
{title}
- } - - return ( -
- - - - - - column.toggleSorting(false)}> - - Asc - - column.toggleSorting(true)}> - - Desc - - {/* - column.toggleVisibility(false)}> - - Hide - */} - - -
- ) -} diff --git a/resources/js/components/dialogs/area-forms/upload-area-form.tsx b/resources/js/components/dialogs/area-forms/upload-area-form.tsx index 8e7072fe5..ad5aa27e3 100644 --- a/resources/js/components/dialogs/area-forms/upload-area-form.tsx +++ b/resources/js/components/dialogs/area-forms/upload-area-form.tsx @@ -2,12 +2,13 @@ import InputError from '@/components/input-error'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { Label } from '@/components/ui/label'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogClose } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; +import { formatBytes, usePdfCompressor } from '@/hooks/use-pdf-compressor'; import { AreaForms, Program } from '@/types'; import { useForm } from '@inertiajs/react'; -import React from 'react'; +import { CheckCircle2, FileText, Loader2, Upload, X } from 'lucide-react'; +import React, { useState } from 'react'; import { toast } from 'sonner'; interface UploadAreaFormProps { @@ -26,29 +27,46 @@ export function UploadAreaForm({ program, form, area_id, onClose }: UploadAreaFo document: null, }); - console.log(form); + const { compress, isCompressing, progress } = usePdfCompressor(); + const [compressionInfo, setCompressionInfo] = useState<{ savedPercent: number; compressedSize: number } | null>(null); - const [isUploading, setIsUploading] = React.useState(false); + const handleFileChange = async (file: File | null) => { + if (!file) { + setData('document', null); + setCompressionInfo(null); + return; + } + + const result = await compress(file); + setData('document', result.file); + + if (!result.skipped && result.savedPercent > 0) { + setCompressionInfo({ savedPercent: result.savedPercent, compressedSize: result.compressedSize }); + } + }; const uploadAreaForm = (e: React.FormEvent) => { - console.log(data); e.preventDefault(); - setIsUploading(true); + + const programLevelId = Array.isArray(program.levels) + ? (program.levels as any)[0]?.accreditation_level_id + : (program.levels as any)?.accreditation_level_id; + post( route('manage.area.upload.area.form.file', { program_id: program.program_id, - level_id: program.levels[0]?.accreditation_level_id, + level_id: programLevelId, area_id: area_id, form_id: form.area_form_id, }), { - onProgress: (progress) => { - if (progress?.percentage) { + onProgress: (p) => { + if (p?.percentage) { toast.info('Uploading...', { description: (
- -

{progress.percentage}%

+ +

{p.percentage}%

), id: 'uploading', @@ -58,7 +76,7 @@ export function UploadAreaForm({ program, form, area_id, onClose }: UploadAreaFo onSuccess: () => { toast.dismiss('uploading'); reset(); - setIsUploading(false); + setCompressionInfo(null); onClose(); }, onError: (errors) => { @@ -66,85 +84,150 @@ export function UploadAreaForm({ program, form, area_id, onClose }: UploadAreaFo toast.error('Failed to upload document', { description: errors.document ?? 'There was an error uploading the document.', }); - setIsUploading(false); }, }, ); }; + const isBusy = isCompressing || processing; + return ( - onClose()}> - - - {form.file_name ? 'Update' : 'Upload'} Document - Upload a Document for this card + !open && onClose()}> + + {/* ── Header ── */} + +
+
+ +
+
+ + {form.file_name ? 'Update' : 'Upload'} Document + + + Upload a document for this card + +
+ +
-
-
-
-
- {!data.document ? ( -
); - console.log(faculties); return ( diff --git a/resources/js/pages/accreditor/accreditor-view.tsx b/resources/js/pages/accreditor/accreditor-view.tsx index 566efabdc..e3ebcfd44 100644 --- a/resources/js/pages/accreditor/accreditor-view.tsx +++ b/resources/js/pages/accreditor/accreditor-view.tsx @@ -232,7 +232,6 @@ export default function AccreditorDashboard() { const program = programs.find((p) => p.id === programId); if (!program) return; - console.log(`Exporting "${program.program_name}" as ${type.toUpperCase()}`); alert(` Exporting ${program.program_name} as ${type.toUpperCase()}`); @@ -256,7 +255,6 @@ export default function AccreditorDashboard() { const area = programs.flatMap(p => p.assigned_areas).find(a => a.id === areaId); if (!area) return; - console.log(`Exporting "${area.area_name}" as ${type.toUpperCase()}`); alert(`Exporting ${area.area_name} as ${type.toUpperCase()}`); setExportAreaDropdown((prev) => ({ ...prev, [areaId]: false })); diff --git a/resources/js/pages/area.tsx b/resources/js/pages/area.tsx index ec0bc15cb..ef8588e51 100644 --- a/resources/js/pages/area.tsx +++ b/resources/js/pages/area.tsx @@ -3,13 +3,14 @@ import PageHeader from '@/components/guest-page-header'; import { buildOutlineTree, RecursiveOutline } from '@/components/recursive-outline'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import Layout from '@/layouts/landing-layout'; -import type { Area, ParameterOutlineCategory, PerProgram } from '@/types'; -import { Head, usePage, usePoll } from '@inertiajs/react'; +import type { Area, ParameterOutlineCategory, Program } from '@/types'; +import { Head, usePage } from '@inertiajs/react'; +import { useSmartPoll } from '@/hooks/use-smart-poll'; import { Construction } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; interface AreaProps { - program: PerProgram; + program: Program; area: Area; categories: ParameterOutlineCategory[]; } @@ -25,12 +26,11 @@ const EmptyState = ({ title, description }: { title: string; description: string const alphaRegex = new RegExp('^[A-Za-z]'); export default function AreaPage({ program, area, categories }: AreaProps) { - usePoll(5000); + useSmartPoll(5000); const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null; const searchKeyword = searchParams?.get('search') || ''; - const { auth } = usePage().props; - const user = auth.user; + const { auth } = usePage().props; const [viewerOpen, setViewerOpen] = useState(false); const [viewerFile, setViewerFile] = useState({ url: '', title: '' }); @@ -40,12 +40,6 @@ export default function AreaPage({ program, area, categories }: AreaProps) { setViewerOpen(true); }; - function highlight(text: string) { - if (!searchKeyword) return text; - const regex = new RegExp(`(${searchKeyword})`, 'gi'); - return text.replace(regex, '$1'); - } - const [openItem, setOpenItem] = useState(undefined); const parameterId = searchParams?.get('parameter'); @@ -65,6 +59,8 @@ export default function AreaPage({ program, area, categories }: AreaProps) { }, 150); }, [parameterId]); + const activeLevel = Array.isArray(program.levels) ? program.levels[0] : program.levels; + return ( <>
- {area.area_forms?.length > 0 ? ( + {(area as any).area_forms?.length > 0 ? (
- {area.area_forms?.map((area_form) => ( + {(area as any).area_forms?.map((area_form: any) => (
- {area.area_parameters?.length > 0 ? ( + {(area as any).area_parameters?.length > 0 ? ( - {[...area.area_parameters] + {[...(area as any).area_parameters] .sort((a, b) => { if (a.parameter_name?.trim().toUpperCase() === 'A') return -1; if (b.parameter_name?.trim().toUpperCase() === 'A') return 1; return a.parameter_name?.localeCompare(b.parameter_name || '') || 0; }) - .map((parameter) => { + .map((parameter: any) => { const paramKey = parameter.parameter_id ?? parameter.area_parameter_id; + const hasOutlines = categories.some((category) => { + const outlines = + parameter.parameter_outlines?.filter( + (outline: any) => + outline.parameter_outline_category_id === + category.parameter_outline_category_id, + ) || []; + return outlines.length > 0; + }); + return ( -
(parameterRef.current[paramKey] = el)} - > - { parameterRef.current[paramKey] = el; }} > - -
-

- {!parameter.parameter_name?.trim() - ? `${parameter.parameter_description}` - : `Parameter ${parameter.parameter_name.toUpperCase()[0]}` - } -

-

- {parameter.parameter_name?.trim() ? parameter.parameter_description : ''} -

-
-
- - {categories.some((category) => { - const outlines = - parameter.parameter_outlines?.filter( - (outline) => - outline.parameter_outline_category_id === - category.parameter_outline_category_id, - ) || []; - return outlines.length > 0; - }) ? ( - categories.map((category) => { - const outlines = - parameter.parameter_outlines?.filter( - (outline) => - outline.parameter_outline_category_id === - category.parameter_outline_category_id, - ) || []; - if (outlines.length === 0) return null; + + +
+

+ {!parameter.parameter_name?.trim() + ? `${parameter.parameter_description}` + : `Parameter ${parameter.parameter_name.toUpperCase()[0]}` + } +

+

+ {parameter.parameter_name?.trim() ? parameter.parameter_description : ''} +

+
+
+ + {hasOutlines ? ( + categories.map((category) => { + const filteredOutlines = (parameter.parameter_outlines || []) + .filter((o: any) => o.parameter_outline_category_id === category.parameter_outline_category_id) + .map((o: any) => ({ + ...o, + initial: category.category_name === 'No Category' + ? (parameter.parameter_name?.trim() ? parameter.parameter_name.toUpperCase().match(alphaRegex)?.[0] || '' : '') + : category.category_name.match(alphaRegex)?.[0] || '' + })); - outlines.map((outline) => { - outline.initial = - category.category_name === 'No Category' - ? parameter.parameter_name === ' ' - ? '' - : parameter.parameter_name.toUpperCase().match(alphaRegex) - : category.category_name.match(alphaRegex); - }); + if (filteredOutlines.length === 0) return null; - const sortedOutlines = buildOutlineTree({ outlines }); + const sortedOutlines = buildOutlineTree({ outlines: filteredOutlines }); - return ( -
-

- {category.category_name === 'No Category' - ? '' - : category.category_name} -

- -
- ); - }) - ) : ( -
-
- - - -

No outline available for this parameter

-

- Content will be added during the accreditation process -

-
-
- )} -
-
-
+ return ( +
+

+ {category.category_name === 'No Category' ? '' : category.category_name} +

+ { + if (params.type === 'view') { + openViewer(params.benchmark.area_files?.file_path || '', params.benchmark.outline_description || ''); + } + }} + resolveBenchDialog={() => {}} // Non-form view is read-only + /> +
+ ); + }) + ) : ( +
+
+ + + +

No outline available for this parameter

+

+ Content will be added during the accreditation process +

+
+
+ )} + + +
); })} diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index 82e171691..7a8dbd2ac 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -53,7 +53,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
-
+
Or, return to log in
diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index 41076e1b1..f99546a8b 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -125,9 +125,9 @@ export default function Login({ status, canResetPassword }: LoginPageProps) { > -
+
- + {(a11y) => ( setData('email', e.target.value)} + className="rounded-xl border-gray-200 focus-visible:ring-[#7f1414]" /> )} @@ -150,7 +151,7 @@ export default function Login({ status, canResetPassword }: LoginPageProps) { Forgot password? @@ -167,6 +168,7 @@ export default function Login({ status, canResetPassword }: LoginPageProps) { placeholder="Password" value={data.password} onChange={(e) => setData('password', e.target.value)} + className="rounded-xl border-gray-200 focus-visible:ring-[#7f1414]" /> )} @@ -175,36 +177,38 @@ export default function Login({ status, canResetPassword }: LoginPageProps) { id="remember" name="remember" tabIndex={3} - className="rounded" + className="rounded-md border-gray-300 data-[state=checked]:bg-[#7f1414] data-[state=checked]:border-[#7f1414]" checked={data.remember} onCheckedChange={(checked) => setData('remember', checked === true)} /> -
{status && ( -

- {status} -

+
+

+ {status} +

+
)} ); diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index a2db1aed4..2edd04b83 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -2,7 +2,6 @@ import AppLayout from '@/layouts/app-layout'; import { SharedData, User, type BreadcrumbItem } from '@/types'; import { Head, usePage } from '@inertiajs/react'; import { DataTable } from '@/components/charts/data-table'; -import GuideTour from "@/pages/test/GuideTour"; import { type ActivityLogs, type DocumentStatistics, type FrequencyUploads, type OverallUploads } from '@/types/dashboard'; import { columns } from '@/components/charts/data-table-columns/logs'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -138,7 +137,6 @@ export default function Dashboard({ frequencyUploads, documentStatistics, overal documentStatistics={activeStatistics} /> -
diff --git a/resources/js/pages/document/ratings.tsx b/resources/js/pages/document/ratings.tsx index 12ab9e370..0429b939b 100644 --- a/resources/js/pages/document/ratings.tsx +++ b/resources/js/pages/document/ratings.tsx @@ -204,7 +204,6 @@ export default function Ratings() { const program = programs.find((p) => p.id === programId); if (!program) return; - console.log(`Exporting "${program.program_name}" as ${type.toUpperCase()}`); alert(` Exporting ${program.program_name} as ${type.toUpperCase()}`); @@ -228,7 +227,6 @@ export default function Ratings() { const area = programs.flatMap((p) => p.assigned_areas).find((a) => a.id === areaId); if (!area) return; - console.log(`Exporting "${area.area_name}" as ${type.toUpperCase()}`); alert(`Exporting ${area.area_name} as ${type.toUpperCase()}`); setExportAreaDropdown((prev) => ({ ...prev, [areaId]: false })); diff --git a/resources/js/pages/manage-programs.tsx b/resources/js/pages/manage-programs.tsx index f95e6e2e8..2dbe1c575 100644 --- a/resources/js/pages/manage-programs.tsx +++ b/resources/js/pages/manage-programs.tsx @@ -10,7 +10,8 @@ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, Di import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import AppLayout from '@/layouts/app-layout'; import type { BreadcrumbItem, PerProgramUnderSurvey } from '@/types'; -import { Head, router, usePage, usePoll } from '@inertiajs/react'; +import { Head, router, usePage } from '@inertiajs/react'; +import { useSmartPoll } from '@/hooks/use-smart-poll'; import { Archive, BookCheck, Edit, FilePlus, Folders, GraduationCap, NotebookIcon, PlusCircleIcon, ScrollText } from 'lucide-react'; import { PageTitle } from '@/components/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -32,7 +33,7 @@ export default function ManagePrograms({ programs }: ProgramsProps) { const role = user.roles.role_name; const assignedPrograms = auth.programs; - usePoll(5000); + useSmartPoll(5000); const [activeTab, setActiveTab] = useState('active'); const [searchTerm, setSearchTerm] = useState(''); diff --git a/resources/js/pages/others.tsx b/resources/js/pages/others.tsx index 597b58a46..c5f31bac1 100644 --- a/resources/js/pages/others.tsx +++ b/resources/js/pages/others.tsx @@ -1,7 +1,8 @@ import PageHeader from '@/components/guest-page-header'; import Layout from '@/layouts/landing-layout'; import { ContentPages, OtherServices } from '@/types/content'; -import { Head, usePoll } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; +import { useSmartPoll } from '@/hooks/use-smart-poll'; import { Construction, Link } from 'lucide-react'; interface OtherServicesProps { @@ -10,7 +11,7 @@ interface OtherServicesProps { } export default function Others({ page, others }: OtherServicesProps) { - usePoll(5000); + useSmartPoll(5000); const EmptyState = ({ title, diff --git a/resources/js/pages/programs.tsx b/resources/js/pages/programs.tsx index 7308bf0d8..5cd4d9946 100644 --- a/resources/js/pages/programs.tsx +++ b/resources/js/pages/programs.tsx @@ -1,7 +1,8 @@ import PageHeader from '@/components/guest-page-header'; import Layout from '@/layouts/landing-layout'; import type { ProgramsUnderSurvey } from '@/types'; -import { Head, Link, usePage, usePoll } from '@inertiajs/react'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { useSmartPoll } from '@/hooks/use-smart-poll'; import { Construction, ImageOff } from 'lucide-react'; interface ProgramsProps { @@ -12,7 +13,7 @@ export default function Programs({ programs }: ProgramsProps) { const { auth } = usePage().props; const user = auth.user; - usePoll(5000); + useSmartPoll(5000); const EmptyState = ({ title, description }: { title: string; description: string }) => (
diff --git a/resources/js/pages/programview.tsx b/resources/js/pages/programview.tsx index ba6b1b6db..bec31b1b3 100644 --- a/resources/js/pages/programview.tsx +++ b/resources/js/pages/programview.tsx @@ -5,7 +5,8 @@ import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import Layout from '@/layouts/landing-layout'; import { PerProgramUnderSurvey } from '@/types'; -import { Head, router, usePage, usePoll, useRemember } from '@inertiajs/react'; +import { Head, router, usePage, useRemember } from '@inertiajs/react'; +import { useSmartPoll } from '@/hooks/use-smart-poll'; import { AlertCircle, BookOpen, @@ -148,8 +149,27 @@ const FacultyCard = forwardRef(({ faculty, isL FacultyCard.displayName = 'FacultyCard'; +const FacultyCardWrapper = ({ f, i, facultyLoading }: { f: any; i: number; facultyLoading: boolean }) => { + const [cardRef, cardInView] = useInView(0.2); + return ( + + ); +}; + export default function Programs({ program }: PerProgramProps) { - usePoll(5000); + useSmartPoll(5000); const [level, setLevel] = useRemember(program.levels[0]?.level, 'level'); const [loading, setLoading] = useState(false); @@ -425,27 +445,14 @@ export default function Programs({ program }: PerProgramProps) { /> ) : (
- {program.faculty_staff?.map((f, i) => { - const [cardRef, cardInView] = useInView(0.2); - return ( - - - ); - })} + {program.faculty_staff?.map((f, i) => ( + + ))}
)}
diff --git a/resources/js/pages/settings/archive.tsx b/resources/js/pages/settings/archive.tsx index d2b085d36..7bd816fc0 100644 --- a/resources/js/pages/settings/archive.tsx +++ b/resources/js/pages/settings/archive.tsx @@ -176,12 +176,10 @@ export default function ArchiveComponent() { } const handleRestore = () => { - console.log("Restoring items:", selectedItems) setSelectedItems([]) } const handlePermanentDelete = () => { - console.log("Permanently deleting items:", selectedItems) setSelectedItems([]) } diff --git a/resources/js/pages/test/GuideTour.tsx b/resources/js/pages/test/GuideTour.tsx deleted file mode 100644 index 9fb28e7b0..000000000 --- a/resources/js/pages/test/GuideTour.tsx +++ /dev/null @@ -1,157 +0,0 @@ -"use client"; - -import { useEffect, useRef } from "react"; -import Shepherd from "shepherd.js"; -import "shepherd.js/dist/css/shepherd.css"; -import "@/pages/test/shepherd-custom.scss"; -import { usePage } from "@inertiajs/react"; - - - - -const GuideTour = () => { - const { url } = usePage(); - const tourRef = useRef(null); - - useEffect(() => { - const tour = new Shepherd.Tour({ - useModalOverlay: true, - defaultStepOptions: { - // UI/UX Improvement: Enable the close icon for easy exit - // cancelIcon: { enabled: true }, - classes: "shepherd-theme-custom", - scrollTo: { behavior: "smooth", block: "center" }, - arrow: false, // You may enable this if you need the arrow - }, - }); - - tourRef.current = tour; - - // --- Dashboard steps --- - if (url === "/dashboard") { - tour.addStep({ - id: "d1", - title: "Welcome to the Dashboard!", - text: "This tour will show you the key areas of the dashboard. Click 'Start tour' to begin, or press F1 anytime to open and esc to exit.", - attachTo: { element: ".dashboard-title", on: "bottom" }, - buttons: [ - { - text: "Start tour", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "d2", - title: "Stats Overview", - text: "Your current performance metrics are summarized here. Keep an eye on these key indicators!", - attachTo: { element: "#stats-card", on: "right" }, - buttons: [ - { - text: "Next", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "d3", - title: "Document Activity Trend", - text: "A line graph timeline where you can track the frequency of document uploads.", - attachTo: { element: "#stats-card-left", on: "right" }, - buttons: [ - { - text: "Next", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "d4", - title: "Document Activity Trend", - text: "A line graph timeline where you can track the frequency of document uploads.", - attachTo: { element: "#stats-card-center", on: "right" }, - buttons: [ - { - text: "Next", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "d5", - title: "Document Activity Trend", - text: "A line graph timeline where you can track the frequency of document uploads.", - attachTo: { element: "#stats-card-right", on: "right" }, - buttons: [ - { - text: "Next", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "d6", - title: "Document Activity Trend", - text: "A line graph timeline where you can track the frequency of document uploads.", - attachTo: { element: "#stat-table", on: "right" }, - buttons: [ - { - text: "Finish", - action: tour.complete, - }, - ], - }); - } - - // --- Programs page steps --- - if (url === "/users") { - tour.addStep({ - id: "p1", - title: "Welcome to User Management Page!", - text: "This page will handle the account creations of users within the system", - attachTo: { element: "", on: "bottom" }, - buttons: [ - { - text: "Next", - action: tour.next, - }, - ], - }); - - tour.addStep({ - id: "p2", - title: "Create Program", - text: "Click this button to quickly add a new program and define its core parameters.", - attachTo: { element: "#user-table", on: "bottom" }, - buttons: [ - { - text: "Finish", - action: tour.complete, - }, - ], - }); - } - - // --- F1 keyboard activation --- - const handleKey = (e: KeyboardEvent) => { - // Note: The key is "F1" (case-sensitive for e.key) - if (e.key === "F1") { - e.preventDefault(); - // Only start if the tour is not already active - if (!tour.isActive()) tour.start(); - } - }; - - window.addEventListener("keydown", handleKey); - return () => window.removeEventListener("keydown", handleKey); - }, [url]); - - return null; // no extra button needed; F1 triggers it -}; - -export default GuideTour; \ No newline at end of file diff --git a/resources/js/pages/test/shepherd-custom.scss b/resources/js/pages/test/shepherd-custom.scss deleted file mode 100644 index 6faf98a70..000000000 --- a/resources/js/pages/test/shepherd-custom.scss +++ /dev/null @@ -1,160 +0,0 @@ -/* Shepherd.js Full Customization Template */ - -// --- Variables (Use SCSS variables for easy theming!) --- - -// 1. COLORS -$step-bg-color: #ffffff; -$step-text-color: #1f2937; -$step-muted-text: #6b7280; -$step-shadow-color: rgba(0, 0, 0, 0.1); - -$primary-btn-bg: #b91c1c; // Example Red -$primary-btn-text: #ffffff; -$secondary-btn-bg: transparent; - -// 2. DIMENSIONS -$border-radius: 0.75rem; -$padding: 1.5rem; -$btn-radius: 0.5rem; -$arrow-size: 10px; - -.shepherd-theme-custom .shepherd-target { - border-radius: 12px; // rounded corners around highlighted element - box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.5); // blue ring - transition: box-shadow 0.3s; -} - -// --- MODAL OVERLAY --- -// This covers the entire screen behind the step. -.shepherd-modal-overlay-container { - // Required to place the tour step behind other elements (e.g., header z-index: 1000) - z-index: 998 !important; - - .shepherd-modal-overlay { - opacity: 0.75 !important; // Adjust opacity - background-color: #000000 !important; // Dark background color - } -} - -// --- STEP CONTAINER (.shepherd-theme-custom) --- -.shepherd-theme-custom { - // Required to place the tour step behind other elements - z-index: 999 !important; - - box-shadow: - 0 4px 6px -1px $step-shadow-color,12 - 0 2px 4px -2px $step-shadow-color !important; - border-radius: $border-radius !important; - - // --- Content Wrapper (Holds background/padding) --- - .shepherd-content { - background-color: $step-bg-color !important; - border-radius: $border-radius !important; - padding: 0 !important; - } - - // --- 1. HEADER (Title and Close Icon) --- - .shepherd-header { - // Layout - display: flex !important; - justify-content: space-between !important; - align-items: center !important; - border-radius: $border-radius $border-radius 0 0 !important; - background-color: #ffffff00 !important; - // Padding (Top, Right, Bottom, Left) - padding: $padding $padding 0.75rem $padding !important; - } - - // --- Title Text --- - .shepherd-title { - color: $step-text-color !important; - font-weight: 600 !important; - font-size: 1rem !important; - margin: 0 !important; - } - - // --- Cancel/Close Icon --- - .shepherd-cancel-icon { - color: $step-muted-text !important; - transition: color 0.2s ease !important; - - &:hover { - color: $step-text-color !important; - } - } - - // --- 2. BODY TEXT --- - .shepherd-text { - color: $step-muted-text !important; - font-size: 0.9rem !important; - line-height: 1.6 !important; - - // Padding (Top, Right, Bottom, Left) - padding: 0.75rem $padding $padding $padding !important; - border-top: 1px solid !important; - } - - // --- 3. FOOTER (Buttons) --- - .shepherd-footer { - display: flex !important; - justify-content: flex-end !important; - gap: 0.75rem !important; - - // Red Footer Background (Matching your design) - background-color: $primary-btn-bg !important; - padding: 0.75rem $padding $padding $padding !important; - border-bottom-left-radius: $border-radius !important; - border-bottom-right-radius: $border-radius !important; - border-top: none !important; - - // --- Buttons (Targeted by Shepherd classes) --- - .shepherd-button { - padding: 2 !important; - border-radius: $btn-radius !important; - font-weight: 600 !important; - border: none !important; - transition: all 0.2s ease !important; - - - - // Primary Button: White Fill on Red Footer - &.shepherd-button-primary { - background-color: #612828 !important; - color: $primary-btn-bg !important; - } - - // Secondary Button (e.g., 'Back' or 'Cancel' button) - &.shepherd-button-secondary { - background-color: transparent !important; - color: #ffffff !important; - border: 1px solid #ffffff !important; - } - - // Fallback for first button (if not primary) - &:nth-child(1):not(.shepherd-button-primary) { - background-color: transparent !important; - color: #ffffff !important; - border: 1px solid #ffffff !important; - } - } - } - - // --- 4. ARROW/TOOLTIP POINTER --- - .shepherd-arrow, - .shepherd-arrow:before { - background: #b91c1c !important; // Arrow matches white body - box-shadow: - 0 4px 6px -1px black ,12 - 0 2px 4px -2px black !important; - } - - - - // // Remove arrow box-shadow for simple look - // &[data-popper-placement^='top'] .shepherd-arrow:before, - // &[data-popper-placement^='bottom'] .shepherd-arrow:before, - // &[data-popper-placement^='left'] .shepherd-arrow:before, - // &[data-popper-placement^='right'] .shepherd-arrow:before { - // box-shadow: none !important; - // } -} diff --git a/resources/js/pages/user-management.tsx b/resources/js/pages/user-management.tsx index f12624c77..1aaa5a3f3 100644 --- a/resources/js/pages/user-management.tsx +++ b/resources/js/pages/user-management.tsx @@ -8,8 +8,7 @@ import { AssignablePrograms, AssignableRoles, type UserRecords } from '@/types/u import { Head } from '@inertiajs/react'; import { SquareUserIcon, User2, User2Icon, UserPlus } from 'lucide-react'; import { useState } from 'react'; -import GuideTour from "@/pages/test/GuideTour"; -import { ActionCard, PageTitle } from '@/components/page-header'; +import { PageTitle } from '@/components/page-header'; interface UsersProps { @@ -55,7 +54,6 @@ export default function Users({ userRecords, programRoles, roles }: UsersProps) return ( <> -
diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index a8dadc695..88e9b0dc5 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -15,7 +15,6 @@ const Link = ({ href, children, ...props }: { href: string; children: React.Reac const router = { visit: (url: string, options: any) => { - console.log(`Mock Router: visit "${url}" with options:`, options); }, }; @@ -456,8 +455,6 @@ export default function Welcome({ page, carousel_images }: LandingProps) { }, []); const carousel_paths = carousel_images.map((img) => img.image_path); - // console.log(carousel_images[0].image_path); - // console.log(carousel_images); return ( <> diff --git a/resources/js/types/files.ts b/resources/js/types/files.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/js/types/guest.ts b/resources/js/types/guest.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/js/types/index.ts b/resources/js/types/index.ts index 753ecfc64..c11183588 100644 --- a/resources/js/types/index.ts +++ b/resources/js/types/index.ts @@ -4,6 +4,7 @@ import { FacultyStaff } from './content'; export interface Auth { user: User; programs: ProgramPrivilege; + [key: string]: unknown; } export interface BreadcrumbItem { @@ -31,56 +32,8 @@ export interface SharedData { } export interface GuestNavigation { - programs: NavPrograms[]; + programs: Program[]; outlines?: ParameterOutlines[]; - [key: string]: unknown; // This allows for additional properties... -} - -export interface NavPrograms { - program_id: number; - program_name: string; - program_link: string; - [key: string]: unknown; // This allows for additional properties... -} - -//Redundant -export interface ProgramsUnderSurvey { - program_id: number; - program_name: string; - degree_type: string; - program_description: string; - program_image_name: string; - program_image_path: string; - program_link: string; - levels?: AccreditationLevels; - active_levels?: AccreditationLevels; - [key: string]: unknown; // This allows for additional properties... -} - -//Redundant -export interface PerProgramUnderSurvey { - program_id: number; - degree_type: string; - program_name: string; - program_link: string; - program_description: string; - under_survey: boolean; - program_image_name: string; - program_image_path: string; - is_active: boolean; - color?: string; - levels?: AccreditationLevels[]; - latest_level?: AccreditationLevels; - faculty_staff?: FacultyStaff[]; - objectives: ProgramObjectives[]; - gallery: ProgramGalleryImages[]; - [key: string]: unknown; -} - -export interface ProgramPrivilege { - program_name: string; - program_link: string; - latest_level?: AccreditationLevels; [key: string]: unknown; } @@ -94,32 +47,44 @@ export interface User { created_at: string; updated_at: string; roles?: Roles; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface Roles { role_id: number; role_name: string; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } -//Redundant -export interface PerProgram { +export interface Program { program_id: number; degree_type: string; program_name: string; program_link: string; - program_description: string; - under_survey: boolean; - program_image_name: string; - program_image_path: string; - levels?: AccreditationLevels[]; + program_description?: string; + program_image_name?: string; + program_image_path?: string; + under_survey?: boolean; + is_active?: boolean; + color?: string; + // Handle the ambiguity: allow both single object and array for flexibility during transition + // but ideally we should move towards a consistent one. + levels?: AccreditationLevels | AccreditationLevels[]; + active_levels?: AccreditationLevels | AccreditationLevels[]; + latest_level?: AccreditationLevels; faculty_staff?: FacultyStaff[]; objectives?: ProgramObjectives[]; gallery?: ProgramGalleryImages[]; [key: string]: unknown; } +export interface ProgramPrivilege { + program_name: string; + program_link: string; + latest_level?: AccreditationLevels; + [key: string]: unknown; +} + export interface ProgramObjectives { program_objective_id: number; program_id: number; @@ -151,7 +116,6 @@ export interface AccreditationLevels { [key: string]: unknown; } -//Redundand export interface ProgramAreas { area_id: number; program_id: number; @@ -168,17 +132,6 @@ export interface ProgramAreas { [key: string]: unknown; } -//Redundant -export interface Program { - program_id: number; - degree_type: string; - program_name: string; - program_link: string; - levels?: AccreditationLevels; - [key: string]: unknown; // This allows for additional properties... -} - -//Redundant export interface Area { area_id: number; program_id: number; @@ -192,7 +145,7 @@ export interface Area { areaParameters?: AreaParameters[]; areaForms?: AreaForms[]; levels?: AccreditationLevels; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface AreaParameters { @@ -202,7 +155,7 @@ export interface AreaParameters { parameter_description: string; area?: Area; parameter_outlines?: ParameterOutlines[]; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface ParameterOutlines { @@ -216,7 +169,7 @@ export interface ParameterOutlines { area_parameters?: AreaParameters; paramter_outline_category?: ParameterOutlineCategory; area_files?: AreaFiles; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface ParameterOutlineCategory { @@ -224,7 +177,7 @@ export interface ParameterOutlineCategory { category_name: string; category_description: string; parameter_outlines?: ParameterOutlines[]; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface AreaFiles { @@ -236,7 +189,7 @@ export interface AreaFiles { file_rejection_reason: string | null; parameter_outlines?: ParameterOutlines; file_status?: FileStatus; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface FileStatus { @@ -244,7 +197,7 @@ export interface FileStatus { status_name: string; area_files?: AreaFiles[]; areaForms?: AreaForms[]; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface AreaForms { @@ -260,14 +213,14 @@ export interface AreaForms { area_form_category?: AreaFormCategory; area?: Area; file_status?: FileStatus; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface AreaFormCategory { area_form_category_id: number; category_name: string; areaForms?: AreaForms[]; - [key: string]: unknown; // This allows for additional properties... + [key: string]: unknown; } export interface FilesOverview { diff --git a/resources/js/types/toast.ts b/resources/js/types/toast.ts deleted file mode 100644 index 9caca6a58..000000000 --- a/resources/js/types/toast.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface NotificationContent { - type: string; - title: string; - message: string; -} - -export interface NotifProps { - flash?: NotificationContent; -} diff --git a/resources/js/workers/pdf-compressor.worker.ts b/resources/js/workers/pdf-compressor.worker.ts new file mode 100644 index 000000000..51d104e82 --- /dev/null +++ b/resources/js/workers/pdf-compressor.worker.ts @@ -0,0 +1,177 @@ +/** + * PDF Compressor Web Worker + * + * Performance priorities: + * - Runs entirely off the main thread (zero UI jank) + * - Pages rendered in parallel batches via Promise.all + * - ArrayBuffer ownership transferred (zero-copy) between threads + * - OffscreenCanvas used for GPU-accelerated rendering + * - Files under threshold are passed through untouched + */ + +import * as pdfjs from 'pdfjs-dist'; +import { PDFDocument } from 'pdf-lib'; + +// ── When running inside a Worker, pdfjs must NOT spawn its own child worker. +// Setting an empty string tells pdfjs to run synchronously in this thread. +pdfjs.GlobalWorkerOptions.workerSrc = ''; + +// ── Message contract ────────────────────────────────────────────────────────── + +interface CompressRequest { + type: 'compress'; + buffer: ArrayBuffer; + quality: number; // JPEG quality 0-1 + scale: number; // render resolution scale + skipThresholdBytes: number; +} + +interface ProgressMessage { + type: 'progress'; + page: number; + total: number; +} + +interface CompleteMessage { + type: 'complete'; + buffer: ArrayBuffer; + originalSize: number; + compressedSize: number; + skipped: boolean; +} + +interface ErrorMessage { + type: 'error'; + message: string; +} + +// ── Page rendering ──────────────────────────────────────────────────────────── + +async function renderPageToJpeg( + page: pdfjs.PDFPageProxy, + scale: number, + quality: number, +): Promise { + const viewport = page.getViewport({ scale }); + const canvas = new OffscreenCanvas( + Math.round(viewport.width), + Math.round(viewport.height), + ); + + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Could not get 2D context from OffscreenCanvas'); + + // Render PDF page onto canvas + await page.render({ + canvasContext: ctx as unknown as CanvasRenderingContext2D, + canvas: canvas as unknown as HTMLCanvasElement, + viewport, + }).promise; + + // Export as JPEG blob (GPU-accelerated in supporting browsers) + const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality }); + return new Uint8Array(await blob.arrayBuffer()); +} + +// ── Main handler ────────────────────────────────────────────────────────────── + +self.onmessage = async (event: MessageEvent) => { + const { buffer, quality, scale, skipThresholdBytes } = event.data; + const originalSize = buffer.byteLength; + + // ── Fast-path: skip small files ────────────────────────────────────────── + if (originalSize < skipThresholdBytes) { + const msg: CompleteMessage = { + type: 'complete', + buffer, + originalSize, + compressedSize: originalSize, + skipped: true, + }; + // Transfer ownership back — zero copy + (self as unknown as Worker).postMessage(msg, [buffer]); + return; + } + + try { + // ── Load source PDF ────────────────────────────────────────────────── + const sourceBytes = new Uint8Array(buffer); + const pdf = await pdfjs.getDocument({ data: sourceBytes }).promise; + const numPages = pdf.numPages; + + // ── Render all pages in parallel batches of 4 ─────────────────────── + // Batching avoids holding too many canvases in memory simultaneously. + const BATCH_SIZE = 4; + const pageJpegs: Uint8Array[] = new Array(numPages); + let pagesCompleted = 0; + + for (let batchStart = 1; batchStart <= numPages; batchStart += BATCH_SIZE) { + const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, numPages); + const batchIndices = Array.from( + { length: batchEnd - batchStart + 1 }, + (_, i) => batchStart + i, + ); + + const batchResults = await Promise.all( + batchIndices.map(async (pageNum) => { + const page = await pdf.getPage(pageNum); + const jpeg = await renderPageToJpeg(page, scale, quality); + page.cleanup(); // Free pdfjs internal resources immediately + return { pageNum, jpeg }; + }), + ); + + for (const { pageNum, jpeg } of batchResults) { + pageJpegs[pageNum - 1] = jpeg; + pagesCompleted++; + + const progress: ProgressMessage = { + type: 'progress', + page: pagesCompleted, + total: numPages, + }; + (self as unknown as Worker).postMessage(progress); + } + } + + // ── Assemble compressed PDF from JPEG images ───────────────────────── + const outputPdf = await PDFDocument.create(); + + for (let i = 0; i < numPages; i++) { + const page = await pdf.getPage(i + 1); + const viewport = page.getViewport({ scale }); + const jpgImage = await outputPdf.embedJpg(pageJpegs[i]); + const pdfPage = outputPdf.addPage([viewport.width, viewport.height]); + pdfPage.drawImage(jpgImage, { + x: 0, + y: 0, + width: viewport.width, + height: viewport.height, + }); + } + + // ── Save with compression ───────────────────────────────────────────── + const compressedBytes = await outputPdf.save({ useObjectStreams: true }); + const compressedBuffer = compressedBytes.buffer.slice( + compressedBytes.byteOffset, + compressedBytes.byteOffset + compressedBytes.byteLength, + ) as ArrayBuffer; + + const msg: CompleteMessage = { + type: 'complete', + buffer: compressedBuffer, + originalSize, + compressedSize: compressedBuffer.byteLength, + skipped: false, + }; + (self as unknown as Worker).postMessage(msg, [compressedBuffer]); + + } catch (err) { + // On any error send back the original buffer so upload still works + const msg: ErrorMessage = { + type: 'error', + message: err instanceof Error ? err.message : 'Unknown compression error', + }; + (self as unknown as Worker).postMessage(msg); + } +}; diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php index dd369858e..4ebcb791e 100644 --- a/resources/views/vendor/mail/html/header.blade.php +++ b/resources/views/vendor/mail/html/header.blade.php @@ -5,13 +5,13 @@
- +
-
PUPCON
-
PUP San Juan Accreditation System
+
PUPCON
+
PUP San Juan Accreditation System
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css index 3d3f08a0f..3fb58fb9f 100644 --- a/resources/views/vendor/mail/html/themes/default.css +++ b/resources/views/vendor/mail/html/themes/default.css @@ -5,7 +5,7 @@ body, body *:not(html):not(style):not(br):not(tr):not(code) { box-sizing: border-box; font-family: - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + 'Inter', 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; position: relative; } @@ -139,10 +139,11 @@ img { -premailer-cellspacing: 0; -premailer-width: 570px; background-color: #ffffff; - border-color: #e8e5ef; - border-radius: 8px; + border-color: #e2e8f0; + border-radius: 12px; border-width: 1px; - box-shadow: 0 2px 8px rgba(127, 20, 20, 0.08); + border-style: solid; + box-shadow: none; margin: 0 auto; padding: 0; width: 570px; @@ -231,7 +232,7 @@ img { .button { -webkit-text-size-adjust: none; - border-radius: 6px; + border-radius: 8px; color: #fff; display: inline-block; overflow: hidden; diff --git a/routes/api.php b/routes/api.php index 826d75921..cd9852d35 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,5 +5,5 @@ use App\Http\Controllers\Api\FacebookController; Route::middleware(['api'])->group(function () { - Route::get('/facebook-feed', [FacebookController::class, 'feed']); + Route::get('/updates', [FacebookController::class, 'feed']); }); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 87cb0aed6..3567372d7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,10 +1,14 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; -import { resolve } from 'node:path'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; import laravel from 'laravel-vite-plugin'; import { defineConfig } from 'vite'; import os from 'os'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + function getLocalIp() { const interfaces = os.networkInterfaces(); for (const name in interfaces) { @@ -40,7 +44,7 @@ export default defineConfig(({ mode }) => ({ }, */ resolve: { alias: { - // eslint-disable-next-line no-undef + 'ziggy-js': resolve(__dirname, 'vendor/tightenco/ziggy'), }, }, @@ -48,14 +52,7 @@ export default defineConfig(({ mode }) => ({ host: '0.0.0.0', port: 5173, strictPort: true, - cors: { - origin: [ - 'http://localhost:8000', - 'http://127.0.0.1:8000', - `http://${LAN_IP}:8000`, - ], - credentials: true, - }, + cors: true, hmr: { host: LAN_IP, },