Compare commits

...

10 Commits

Author SHA1 Message Date
Annisa Nurul Hidayatil Jannah 2cbabf0554
Merge pull request #2 from Annisanzn/nisa2
Nisa2
2026-02-17 18:16:30 +07:00
krizzn65 75f48918b8 selesai smeua 2026-02-17 18:15:42 +07:00
krizzn65 049bbd36a8 selesai semuaa 2026-02-17 18:09:50 +07:00
Annisa Nurul Hidayatil Jannah 5cb3bdafcd
Merge pull request #1 from Annisanzn/nisa2
Nisa2
2026-02-17 17:20:53 +07:00
krizzn65 c3ba927c79 hlaman artikel 2026-02-17 17:20:06 +07:00
krizzn65 21065b0ab3 perbaiki halaamn tentang 2026-02-17 14:25:03 +07:00
krizzn65 a55559a202 perbaiki header 2026-02-17 00:15:19 +07:00
krizzn65 282019870d perbaikan header 2026-02-16 11:53:56 +07:00
krizzn65 b90cdc07d7 perbaiki navbar 2026-02-15 19:48:24 +07:00
Annisanzn 8c5ef3e1cd landing page dan modal login tapi masih banyak yang error 2026-02-14 22:05:04 +07:00
65 changed files with 6326 additions and 1565 deletions

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Api;
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Http\Resources\ContactInfoResource;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Api;
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Models\HeroSection;

View File

@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\LoginRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Handle user login
*/
public function login(LoginRequest $request)
{
$credentials = $request->validated();
// Auto-assign role based on email domain
$email = $credentials['email'];
$role = $this->assignRoleByEmail($email);
// Find user by email
$user = User::where('email', $email)->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
return response()->json([
'success' => false,
'message' => 'Email atau password salah'
], 401);
}
// Update user role if changed
if ($user->role !== $role) {
$user->role = $role;
$user->save();
}
// Create token
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'success' => true,
'message' => 'Login berhasil',
'token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'role' => $user->role,
'nim' => $user->nim,
]
]);
}
/**
* Handle user logout
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'success' => true,
'message' => 'Logout berhasil'
]);
}
/**
* Get current user
*/
public function user(Request $request)
{
return response()->json([
'success' => true,
'user' => [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'role' => $request->user()->role,
'nim' => $request->user()->nim,
]
]);
}
/**
* Auto-assign role based on email domain
*/
private function assignRoleByEmail(string $email): string
{
// Student email pattern: nim@student.polije.ac.id
if (str_contains($email, 'student.polije.ac.id')) {
return 'user';
}
// Staff email pattern: name@polije.ac.id
if (str_contains($email, 'polije.ac.id')) {
return 'konselor'; // Default to konselor for staff
}
// Default fallback
return 'user';
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string $role): Response
{
if (!auth()->check()) {
return response()->json([
'success' => false,
'message' => 'Unauthorized'
], 401);
}
$user = auth()->user();
// Check if user has the required role
if ($user->role !== $role) {
return response()->json([
'success' => false,
'message' => 'Forbidden - Insufficient permissions'
], 403);
}
return $next($request);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Contracts\Validation\Validator;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => 'required|email',
'password' => 'required|min:8',
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'email.required' => 'Email wajib diisi',
'email.email' => 'Format email tidak valid',
'password.required' => 'Password wajib diisi',
'password.min' => 'Password minimal 8 karakter',
];
}
/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
$email = $this->input('email');
// Custom validation: email must contain polije.ac.id
if ($email && !str_contains($email, 'polije.ac.id')) {
$validator->errors()->add('email', 'Gunakan email resmi Polije (@polije.ac.id atau @student.polije.ac.id)');
}
});
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'email' => 'Email',
'password' => 'Password',
];
}
}

View File

@ -6,11 +6,12 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasFactory, Notifiable, HasApiTokens;
/**
* The attributes that are mass assignable.
@ -21,6 +22,8 @@ class User extends Authenticatable
'name',
'email',
'password',
'role',
'nim',
];
/**
@ -45,4 +48,28 @@ protected function casts(): array
'password' => 'hashed',
];
}
/**
* Check if user is student
*/
public function isStudent(): bool
{
return $this->role === 'user';
}
/**
* Check if user is konselor
*/
public function isKonselor(): bool
{
return $this->role === 'konselor';
}
/**
* Check if user is operator
*/
public function isOperator(): bool
{
return $this->role === 'operator';
}
}

View File

@ -8,6 +8,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1"
},
"require-dev": {

65
backend/composer.lock generated
View File

@ -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": "c514d8f7b9fc5970bdd94287905ef584",
"content-hash": "ad91ab9bde70e3576f2b64dd11542bc4",
"packages": [
{
"name": "brick/math",
@ -1333,6 +1333,69 @@
},
"time": "2026-02-06T12:17:10+00:00"
},
{
"name": "laravel/sanctum",
"version": "v4.3.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
"reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/console": "^11.0|^12.0|^13.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/database": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php": "^8.2",
"symfony/console": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.15|^10.8|^11.0",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Sanctum\\SanctumServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Sanctum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
"keywords": [
"auth",
"laravel",
"sanctum"
],
"support": {
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2026-02-07T17:19:31+00:00"
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.9",

View File

@ -19,7 +19,7 @@
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175'],
'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175', 'http://localhost:5176', 'http://127.0.0.1:5176'],
'allowed_origins_patterns' => [],

View File

@ -0,0 +1,84 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/
'guard' => ['web'],
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@ -17,24 +17,11 @@ public function up(): void
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->enum('role', ['user', 'konselor', 'operator'])->default('user');
$table->string('nim')->nullable(); // For student users
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
@ -43,7 +30,5 @@ public function up(): void
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -79,10 +79,33 @@ public function run(): void
Article::create($article);
}
// Create test user
User::factory()->create([
'name' => 'Admin PolijeCare',
'email' => 'admin@polije.ac.id',
]);
// Create test users with specific roles
$users = [
[
'name' => 'Ahmad Mahasiswa',
'email' => '2021001@student.polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'user',
'nim' => '2021001',
],
[
'name' => 'Siti Konselor',
'email' => 'siti@polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'konselor',
'nim' => null,
],
[
'name' => 'Budi Operator',
'email' => 'budi@polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'operator',
'nim' => null,
],
];
foreach ($users as $user) {
User::create($user);
}
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create test users
$users = [
[
'name' => 'Ahmad Mahasiswa',
'email' => '2021001@student.polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'user',
'nim' => '2021001',
],
[
'name' => 'Siti Konselor',
'email' => 'siti@polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'konselor',
'nim' => null,
],
[
'name' => 'Budi Operator',
'email' => 'budi@polije.ac.id',
'password' => Hash::make('password123'),
'role' => 'operator',
'nim' => null,
],
];
foreach ($users as $user) {
User::create($user);
}
}
}

View File

@ -2,9 +2,11 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\ArticleController;
use App\Http\Controllers\Api\ContactController;
use App\Http\Controllers\Api\HeroController;
use App\Http\Controllers\API\ArticleController;
use App\Http\Controllers\API\ContactController;
use App\Http\Controllers\API\HeroController;
use App\Http\Controllers\AuthController;
use App\Http\Middleware\RoleMiddleware;
Route::get('/test', function () {
return response()->json([
@ -21,3 +23,54 @@
// Hero Section
Route::get('/hero', [HeroController::class, 'index']);
// Auth routes
Route::post('/login', [AuthController::class, 'login']);
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
// User routes
Route::middleware(RoleMiddleware::class . ':user')->prefix('user')->group(function () {
Route::get('/dashboard', function () {
return response()->json([
'success' => true,
'message' => 'User Dashboard',
'data' => [
'role' => 'user',
'dashboard' => 'student'
]
]);
});
});
// Konselor routes
Route::middleware(RoleMiddleware::class . ':konselor')->prefix('konselor')->group(function () {
Route::get('/dashboard', function () {
return response()->json([
'success' => true,
'message' => 'Konselor Dashboard',
'data' => [
'role' => 'konselor',
'dashboard' => 'konselor'
]
]);
});
});
// Operator routes
Route::middleware(RoleMiddleware::class . ':operator')->prefix('operator')->group(function () {
Route::get('/dashboard', function () {
return response()->json([
'success' => true,
'message' => 'Operator Dashboard',
'data' => [
'role' => 'operator',
'dashboard' => 'operator'
]
]);
});
});
});

36
backend/test-server.php Normal file
View File

@ -0,0 +1,36 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
echo "Backend server is ready!\n";
echo "Available routes:\n";
echo "- GET /api/test\n";
echo "- POST /api/login\n";
echo "- GET /api/user\n";
echo "- GET /api/contact\n";
echo "- GET /api/hero\n";
echo "- GET /api/articles\n";
// Test database connection
try {
\Illuminate\Support\Facades\DB::connection()->getPdo();
echo "\n✅ Database connection: OK\n";
// Check users
$userCount = \App\Models\User::count();
echo "✅ Users in database: {$userCount}\n";
if ($userCount > 0) {
echo "✅ Test users ready for login\n";
}
} catch (\Exception $e) {
echo "\n❌ Database connection failed: " . $e->getMessage() . "\n";
}
echo "\nTo start the server, run: php artisan serve\n";

25
frontend/components.json Normal file
View File

@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@react-bits": "https://reactbits.dev/r/{name}.json"
}
}

10
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,27 @@
"preview": "vite preview"
},
"dependencies": {
"@iconify/react": "^6.0.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"axios": "^1.13.5",
"framer-motion": "^10.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^10.18.0",
"lucide-react": "^0.564.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.5.0",
"react-router-dom": "^6.8.1",
"tailwindcss": "^3.4.19"
"styled-components": "^6.3.9",
"tailwind-merge": "^3.4.1",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

BIN
frontend/public/Gambar1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,55 @@
import React from 'react';
const AppMinimal = () => {
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1 style={{ color: '#E6E6FA', marginBottom: '20px' }}>PolijeCare - Test Page</h1>
<p style={{ color: '#666', marginBottom: '20px' }}>Halaman test untuk memastikan React berfungsi.</p>
<div style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
marginBottom: '20px'
}}>
<h2 style={{ color: '#333', marginBottom: '10px' }}>Status System:</h2>
<ul style={{ color: '#666', lineHeight: '1.6' }}>
<li> React berjalan</li>
<li> CSS terload</li>
<li> Tidak ada black screen</li>
</ul>
</div>
<div style={{
padding: '20px',
backgroundColor: '#E6E6FA',
borderRadius: '8px',
marginBottom: '20px'
}}>
<h2 style={{ color: '#333', marginBottom: '10px' }}>Test Login:</h2>
<p style={{ color: '#666', marginBottom: '10px' }}>Akun test tersedia:</p>
<div style={{ backgroundColor: '#fff', padding: '10px', borderRadius: '4px' }}>
<strong>Mahasiswa:</strong> 2021001@student.polije.ac.id / password123<br/>
<strong>Konselor:</strong> siti@polije.ac.id / password123<br/>
<strong>Operator:</strong> budi@polije.ac.id / password123
</div>
</div>
<button
onClick={() => alert('Test button berfungsi!')}
style={{
padding: '10px 20px',
backgroundColor: '#4C6EF5',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Test Button
</button>
</div>
);
};
export default AppMinimal;

View File

@ -1,29 +1,87 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import { ScrollParticles } from './components/ui/scroll-particles';
// Pages
import LandingPage from './pages/LandingPage';
import ArticleDetail from './components/ArticleDetail';
import './App.css';
import UserDashboard from './pages/UserDashboard';
import KonselorDashboard from './pages/KonselorDashboard';
import OperatorDashboard from './pages/OperatorDashboard';
function App() {
return (
<ThemeProvider>
<Router>
<div className="App">
<ScrollParticles />
<AuthProvider>
<Router>
<Routes>
{/* Public Routes */}
<Route path="/" element={<LandingPage />} />
<Route path="/artikel" element={<LandingPage />} />
<Route path="/artikel/:slug" element={<ArticleDetail />} />
<Route path="/login" element={<LandingPage />} />
<Route path="/user/dashboard" element={<LandingPage />} />
<Route path="/operator/dashboard" element={<LandingPage />} />
<Route path="/konselor/dashboard" element={<LandingPage />} />
<Route path="/redirect" element={<LandingPage />} />
<Route path="/about" element={<LandingPage />} />
<Route path="/services" element={<LandingPage />} />
<Route path="/articles" element={<LandingPage />} />
<Route path="/contact" element={<LandingPage />} />
{/* Protected Dashboard Routes */}
<Route
path="/user/dashboard"
element={
<ProtectedRoute requiredRole="user">
<UserDashboard />
</ProtectedRoute>
}
/>
<Route
path="/konselor/dashboard"
element={
<ProtectedRoute requiredRole="konselor">
<KonselorDashboard />
</ProtectedRoute>
}
/>
<Route
path="/operator/dashboard"
element={
<ProtectedRoute requiredRole="operator">
<OperatorDashboard />
</ProtectedRoute>
}
/>
{/* Redirect */}
<Route path="/redirect" element={<RedirectDashboard />} />
{/* 404 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</Router>
</Router>
</AuthProvider>
</ThemeProvider>
);
}
// Redirect component for role-based navigation
const RedirectDashboard = () => {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated || !user) {
return <Navigate to="/" replace />;
}
switch (user.role) {
case 'user':
return <Navigate to="/user/dashboard" replace />;
case 'konselor':
return <Navigate to="/konselor/dashboard" replace />;
case 'operator':
return <Navigate to="/operator/dashboard" replace />;
default:
return <Navigate to="/" replace />;
}
};
export default App;

View File

@ -1,239 +1,5 @@
import React from 'react';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, slideLeft, staggerChildren } from '../utils/motionVariants';
import AboutSection from "@/components/ui/about-section";
const About = () => {
const features = [
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
),
title: 'Perlindungan',
description: 'Melindungi korban dan saksi dari segala bentuk ancaman atau intimidasi'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
),
title: 'Pendampingan',
description: 'Memberikan dukungan psikologis dan hukum yang dibutuhkan'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
),
title: 'Kerahasiaan',
description: 'Menjaga identitas dan informasi pelapor dengan ketat'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
),
title: 'Keadilan',
description: 'Memastikan proses yang adil dan transparan untuk semua pihak'
}
];
return (
<section id="about" className="py-20 bg-gradient-to-br from-purple-50 to-purple-100 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 right-10 w-64 h-64 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-10 left-10 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Tentang <span className="text-primary">Satgas PPKPT</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Satuan Tugas Pencegahan dan Penanganan Kekerasan Seksual Politeknik Negeri Jember
</motion.p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Left Content - Logo and Description */}
<motion.div
className="space-y-8"
variants={slideLeft}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{/* PolijeCare Logo */}
<div className="flex justify-center lg:justify-start">
<motion.div
className="relative group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3 }}
>
<div className="relative w-56 h-56">
{/* Outer gradient ring */}
<div className="absolute inset-0 bg-gradient-to-br from-primary to-accent rounded-3xl flex items-center justify-center shadow-soft group-hover:shadow-card transition-all duration-300">
<div className="w-48 h-48 bg-white rounded-2xl flex items-center justify-center">
<div className="w-40 h-40 bg-gradient-to-br from-primary/10 to-accent/10 rounded-xl flex items-center justify-center">
<span className="text-2xl font-bold text-primary">PolijeCare</span>
</div>
</div>
</div>
{/* Floating decoration */}
<motion.div
className="absolute -top-3 -right-3 w-10 h-10 bg-accent rounded-2xl shadow-soft flex items-center justify-center"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 10, -10, 0]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
>
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</motion.div>
</div>
</motion.div>
</div>
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<h3 className="text-2xl font-bold text-gray-900">
Layanan Pengaduan dan Pendampingan Terpercaya
</h3>
<p className="text-lg text-gray-600 leading-relaxed">
PolijeCare merupakan kanal resmi pengaduan Satgas PPKPT Politeknik Negeri Jember yang menangani laporan kekerasan seksual secara empati, profesional, dan menjaga kerahasiaan.
</p>
<p className="text-lg text-gray-600 leading-relaxed">
Kami berkomitmen untuk menciptakan lingkungan kampus yang <span className="text-primary font-semibold">aman</span>, <span className="text-primary font-semibold">mendukung</span>, dan <span className="text-primary font-semibold">bebas dari kekerasan seksual</span> bagi seluruh sivitas akademika.
</p>
</motion.div>
{/* Stats */}
<motion.div
className="grid grid-cols-2 gap-6 pt-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<div className="text-center p-4 bg-white rounded-2xl shadow-soft">
<div className="text-3xl font-bold text-primary mb-2">24/7</div>
<div className="text-sm text-gray-600 font-medium">Layanan Darurat</div>
</div>
<div className="text-center p-4 bg-white rounded-2xl shadow-soft">
<div className="text-3xl font-bold text-accent mb-2">100%</div>
<div className="text-sm text-gray-600 font-medium">Rahasia Terjamin</div>
</div>
</motion.div>
</motion.div>
{/* Right Content - Features Grid */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 gap-6"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{features.map((feature, index) => (
<motion.div
key={feature.title}
className="bg-white rounded-2xl p-6 shadow-soft hover:shadow-card transition-all duration-300 hover:-translate-y-1 group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.02 }}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-primary/10 to-accent/10 rounded-xl flex items-center justify-center text-primary group-hover:from-primary/20 group-hover:to-accent/20 transition-all duration-300">
{feature.icon}
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-gray-900 mb-2 group-hover:text-primary transition-colors">
{feature.title}
</h4>
<p className="text-gray-600 text-sm leading-relaxed">
{feature.description}
</p>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Quote */}
<motion.blockquote
className="bg-gradient-to-r from-primary/10 to-accent/10 rounded-2xl p-6 border-l-4 border-primary"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.6 }}
>
<p className="text-lg text-gray-700 italic font-medium leading-relaxed">
"Setiap individu berhak mendapatkan perlindungan dan rasa aman dalam menempuh pendidikan. Mari bersama-sama menjaga kampus kita sebagai tempat yang aman dan mendukung bagi semua."
</p>
<footer className="mt-4 text-sm text-gray-600 font-semibold">
Satgas PPKPT Polije
</footer>
</motion.blockquote>
</motion.div>
</div>
</div>
</section>
);
};
export default About;
export default function About() {
return <AboutSection />;
}

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { articleService } from '../services/articleService';
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
import { fadeIn, slideUp } from '../utils/motionVariants';
import { Gallery4 } from './gallery4';
const Articles = () => {
const [articles, setArticles] = useState([]);
@ -10,229 +10,130 @@ const Articles = () => {
const [error, setError] = useState(null);
useEffect(() => {
fetchArticles();
// Temporary disable API calls to prevent CORS errors
const mockArticles = [
{
id: 1,
title: 'Guru Besar UGM Diduga Lakukan Kekerasan Seksual, Diberhentikan Sementara',
slug: 'https://www.detik.com/jateng/berita/d-6204001/dugaan-kekerasan-seksual-guru-besar-ugm-dipecat-sebagai-dosen',
image: 'https://images.unsplash.com/photo-1592280771800-45cb10bd3dcf?q=80&w=1740&auto=format&fit=crop',
content: 'Universitas Gadjah Mada (UGM) mengambil tindakan tegas dengan memberhentikan sementara seorang guru besar yang diduga terlibat kasus kekerasan seksual.',
is_published: true,
published_at: '2025-01-20T00:00:00.000000Z'
},
{
id: 2,
title: 'Rektor Universitas Pancasila Nonaktif Jalani Pemeriksaan Kasus Pelecehan',
slug: 'https://metro.tempo.co/read/1840000/kasus-pelecehan-seksual-rektor-universitas-pancasila',
image: 'https://images.unsplash.com/photo-1541339907198-e08756dedf3f?q=80&w=1740&auto=format&fit=crop',
content: 'Polda Metro Jaya memeriksa Rektor Universitas Pancasila nonaktif terkait laporan dugaan pelecehan seksual terhadap pegawai kampus.',
is_published: true,
published_at: '2024-06-15T00:00:00.000000Z'
},
{
id: 3,
title: 'Unand Resmikan Satgas PPK, Perluas Cakupan Penanganan Kekerasan',
slug: 'https://www.unand.ac.id/id/berita-peristiwa/berita/item/5799-resmikan-satgas-ppk-rektor-unand-kawal-kampus-aman.html',
image: 'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?q=80&w=1740&auto=format&fit=crop',
content: 'Universitas Andalas meresmikan Satuan Tugas Pencegahan dan Penanganan Kekerasan (PPK) untuk menciptakan lingkungan kampus yang aman dan inklusif.',
is_published: true,
published_at: '2024-11-10T00:00:00.000000Z'
},
{
id: 4,
title: 'Mahasiswa Unsri Tuntut Penuntasan Kasus Pelecehan Seksual',
slug: 'https://www.cnnindonesia.com/nasional/20211203145209-12-729000/mahasiswa-unsri-demo-tuntut-usut-tuntas-dugaan-pelecehan-seksual',
image: 'https://images.unsplash.com/photo-1555848960-8c3af5e4860c?q=80&w=1740&auto=format&fit=crop',
content: 'Ratusan mahasiswa Universitas Sriwijaya menggelar aksi damai menuntut pengusutan tuntas kasus dugaan pelecehan seksual oleh oknum dosen.',
is_published: true,
published_at: '2024-10-05T00:00:00.000000Z'
},
{
id: 5,
title: 'Kemendikbudristek Cabut Izin Kampus yang Abaikan Kasus Kekerasan Seksual',
slug: 'https://nasional.kompas.com/read/2023/06/07/11261391/izin-23-perguruan-tinggi-dicabut-ada-kampus-yang-abaikan-kasus-kekerasan',
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=1740&auto=format&fit=crop',
content: 'Kemendikbudristek mengambil langkah tegas mencabut izin operasional perguruan tinggi yang terbukti melakukan pelanggaran berat, termasuk pembiaran kekerasan seksual.',
is_published: true,
published_at: '2024-02-01T00:00:00.000000Z'
},
{
id: 6,
title: 'Puan Maharani: Kampus Harus Jadi Ruang Aman Bebas Kekerasan Seksual',
slug: 'https://www.dpr.go.id/berita/detail/id/35000/t/Ketua+DPR+Minta+Kampus+Jadi+Ruang+Aman+dari+Kekerasan+Seksual',
image: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?q=80&w=1740&auto=format&fit=crop',
content: 'Ketua DPR RI Puan Maharani menegaskan pentingnya komitmen perguruan tinggi dalam menciptakan ruang aman bebas dari segala bentuk kekerasan seksual.',
is_published: true,
published_at: '2024-09-15T00:00:00.000000Z'
},
{
id: 7,
title: 'Komnas Perempuan: Kekerasan Seksual di Lingkungan Pendidikan Masih Mengkhawatirkan',
slug: 'https://www.kompas.id/baca/humaniora/2024/03/07/kekerasan-seksual-di-lingkungan-pendidikan-masih-tinggi',
image: 'https://images.unsplash.com/photo-1573164713988-8665fc963095?q=80&w=1740&auto=format&fit=crop',
content: 'Komnas Perempuan mencatat angka kekerasan seksual di lingkungan pendidikan masih tinggi dan memerlukan penanganan sistemik yang lebih serius.',
is_published: true,
published_at: '2024-03-08T00:00:00.000000Z'
},
{
id: 8,
title: 'Pentingnya Pendidikan Seksual di Kampus untuk Cegah Tindakan Asusila',
slug: 'https://edukasi.kompas.com/read/2021/11/12/100000371/pentingnya-pendidikan-seksual-sejak-dini-untuk-cegah-pelecehan',
image: 'https://images.unsplash.com/photo-1544531586-fde5298cdd40?q=80&w=1740&auto=format&fit=crop',
content: 'Pendidikan seksual yang komprehensif di lingkungan kampus dinilai efektif sebagai langkah preventif untuk mencegah terjadinya tindak asusila.',
is_published: true,
published_at: '2024-01-10T00:00:00.000000Z'
}
];
setArticles(mockArticles);
setLoading(false);
}, []);
const fetchArticles = async () => {
try {
setLoading(true);
const response = await articleService.getAll();
if (response.success) {
setArticles(response.data);
} else {
setError('Failed to load articles');
}
} catch (err) {
setError('Failed to load articles');
console.error('Error fetching articles:', err);
} finally {
setLoading(false);
}
};
const galleryItems = articles.map(article => ({
id: article.id.toString(),
title: article.title,
description: article.content,
href: article.slug,
image: article.image
}));
const formatDate = (dateString) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(dateString).toLocaleDateString('id-ID', options);
};
const SectionTitle = (
<span>
Artikel & <span className="text-primary">Pengumuman</span>
</span>
);
return (
<section id="articles" className="py-20 bg-white relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 right-10 w-64 h-64 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-10 left-10 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
<div id="articles" className="bg-gray-50 dark:bg-gray-900 min-h-screen relative overflow-hidden">
{/* Background Decorations */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl translate-x-1/3 -translate-y-1/3"></div>
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-blue-500/5 rounded-full blur-3xl -translate-x-1/3 translate-y-1/3"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Artikel & <span className="text-primary">Pengumuman</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Dapatkan informasi terbaru seputar layanan, edukasi, dan pengumuman penting dari Satgas PPKPT Polije.
</motion.p>
</motion.div>
{/* Loading State */}
<div className="relative z-10">
{loading && (
<motion.div
className="flex justify-center items-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="flex flex-col items-center space-y-4">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
<p className="text-gray-600 font-medium">Memuat artikel...</p>
</div>
</motion.div>
<div className="flex justify-center items-center py-32">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
</div>
)}
{/* Error State */}
{error && (
<motion.div
className="text-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="bg-danger/10 border border-danger/20 rounded-2xl p-8 max-w-md mx-auto">
<div className="w-16 h-16 bg-danger/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-danger" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Gagal Memuat Artikel</h3>
<p className="text-gray-600 text-sm mb-4">{error}</p>
<button
onClick={fetchArticles}
className="px-6 py-2 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
>
Coba Lagi
</button>
</div>
</motion.div>
<div className="text-center py-32 text-red-500 max-w-lg mx-auto px-4">
<p>{error}</p>
</div>
)}
{/* Articles Grid */}
{!loading && !error && (
<>
{articles.length === 0 ? (
<motion.div
className="text-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="bg-gray-50 rounded-2xl p-12 max-w-md mx-auto">
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Belum Ada Artikel</h3>
<p className="text-gray-600 text-sm">Belum ada artikel atau pengumuman yang tersedia saat ini.</p>
</div>
</motion.div>
) : (
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{articles.map((article, index) => (
<motion.article
key={article.uuid}
className="bg-white rounded-2xl shadow-soft hover:shadow-card transition-all duration-300 hover:-translate-y-2 overflow-hidden group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
>
{/* Article Image */}
<div className="relative h-48 overflow-hidden">
{article.image ? (
<img
src={article.image}
alt={article.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-primary/10 to-accent/10 flex items-center justify-center">
<svg className="w-16 h-16 text-primary/30" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
{/* Article Content */}
<div className="p-6 space-y-4">
{/* Date */}
<div className="flex items-center text-sm text-gray-500">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/>
</svg>
{formatDate(article.published_at)}
</div>
{/* Title */}
<h3 className="text-xl font-bold text-gray-900 line-clamp-2 group-hover:text-primary transition-colors">
{article.title}
</h3>
{/* Excerpt */}
<p className="text-gray-600 text-sm leading-relaxed line-clamp-3">
{article.excerpt}
</p>
{/* Read More Link */}
<div className="pt-4">
<Link
to={`/artikel/${article.slug}`}
className="inline-flex items-center text-primary font-semibold text-sm hover:text-primary-dark transition-colors group"
>
Baca Selengkapnya
<svg className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
</Link>
</div>
</div>
</motion.article>
))}
</motion.div>
)}
</>
)}
{/* View All Button */}
{!loading && !error && articles.length > 0 && (
<motion.div
className="text-center mt-12"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<Link
to="/artikel"
className="inline-flex items-center px-8 py-4 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold shadow-soft"
>
Lihat Semua Artikel
<svg className="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
</Link>
</motion.div>
<Gallery4
title={SectionTitle}
description="Dapatkan informasi terbaru seputar layanan, edukasi, dan pengumuman penting dari Satgas PPKPT Polije. Kami berkomitmen untuk transparansi dan edukasi."
items={galleryItems}
/>
)}
</div>
</section>
</div>
);
};

View File

@ -1,33 +1,29 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { contactService } from '../services/contactService';
import { MapPin, Phone, Mail, Instagram, MessageCircle, Globe, Clock, ShieldAlert } from 'lucide-react';
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
import { SpotlightCard } from './ui/spotlight-card';
import { FlowButton } from './ui/flow-button';
const Contact = () => {
const [contactInfo, setContactInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchContactInfo();
}, []);
// Temporary disable API calls to prevent 404 errors
const mockContactInfo = {
address: 'Jl. Mastrip PO Box 164, Jember 68121, Jawa Timur, Indonesia',
phone: '+62 331-123456',
email: 'satgasppkpt@polije.ac.id',
instagram: '@satgasppkpt_polije',
whatsapp: '+6281234567890',
facebook: 'SatgasPPKPTPolije',
twitter: '@SatgasPPKPTPolije'
};
const fetchContactInfo = async () => {
try {
setLoading(true);
const response = await contactService.get();
if (response.success) {
setContactInfo(response.data);
} else {
setError('Failed to load contact information');
}
} catch (err) {
setError('Failed to load contact information');
console.error('Error fetching contact info:', err);
} finally {
setLoading(false);
}
};
setContactInfo(mockContactInfo);
setLoading(false);
}, []);
const defaultContact = {
address: 'Jl. Mastrip PO Box 164, Jember 68121, Jawa Timur, Indonesia',
@ -41,250 +37,181 @@ const Contact = () => {
const contactMethods = [
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/>
</svg>
),
label: 'Telepon',
icon: <Phone className="w-6 h-6" />,
label: 'Telepon Kantor',
value: contact.phone,
href: `tel:${contact.phone}`,
color: 'text-primary'
color: 'text-blue-500',
spotlight: 'rgba(59, 130, 246, 0.2)'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
),
label: 'Email',
icon: <Mail className="w-6 h-6" />,
label: 'Email Resmi',
value: contact.email,
href: `mailto:${contact.email}`,
color: 'text-primary'
color: 'text-purple-500',
spotlight: 'rgba(168, 85, 247, 0.2)'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.149-.67.149-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
</svg>
),
label: 'WhatsApp',
icon: <Instagram className="w-6 h-6" />,
label: 'Instagram',
value: contact.instagram,
href: `https://instagram.com/${contact.instagram?.replace('@', '')}`,
color: 'text-pink-500',
spotlight: 'rgba(236, 72, 153, 0.2)'
},
{
icon: <MessageCircle className="w-6 h-6" />,
label: 'WhatsApp Admin',
value: contact.whatsapp,
href: `https://wa.me/${contact.whatsapp?.replace(/[^0-9]/g, '')}`,
color: 'text-accent'
}
];
const socialMedia = [
{
name: 'Instagram',
icon: (
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zM5.838 12a6.162 6.162 0 1112.324 0 6.162 6.162 0 01-12.324 0zM12 16a4 4 0 110-8 4 4 0 010 8zm4.965-10.405a1.44 1.44 0 112.881.001 1.44 1.44 0 01-2.881-.001z"/>
</svg>
),
href: `https://instagram.com/${contact.instagram?.replace('@', '')}`,
color: 'hover:text-pink-600'
color: 'text-green-500',
spotlight: 'rgba(34, 197, 94, 0.2)'
}
];
return (
<section id="contact" className="py-20 bg-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-20 right-20 w-72 h-72 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-20 left-20 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
<section id="contact" className="py-24 bg-gray-50 dark:bg-gray-900 relative overflow-hidden">
{/* Background Decorations */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-[10%] left-[5%] w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-[10%] right-[5%] w-[500px] h-[500px] bg-blue-500/5 rounded-full blur-3xl"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
<motion.div
className="text-center mb-16"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Hubungi <span className="text-primary">Kami</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Kami siap membantu Anda. Jangan ragu untuk menghubungi kami melalui berbagai metode komunikasi yang tersedia.
</motion.p>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed">
Kami siap membantu Anda. Jangan ragu untuk menghubungi kami melalui berbagai saluran komunikasi yang tersedia. Privasi Anda adalah prioritas kami.
</p>
</motion.div>
{/* Loading State */}
{loading && (
<motion.div
className="flex justify-center items-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="flex flex-col items-center space-y-4">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
<p className="text-gray-600 font-medium">Memuat informasi kontak...</p>
</div>
</motion.div>
)}
{/* Contact Content */}
{!loading && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Contact Information */}
<motion.div
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<div className="bg-white rounded-3xl p-8 shadow-soft">
<h3 className="text-2xl font-bold text-gray-900 mb-8">Informasi Kontak</h3>
{/* Contact Methods */}
<div className="space-y-6 mb-8">
{contactMethods.map((method, index) => (
<motion.div
key={index}
className="flex items-start space-x-4 p-4 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left Column: Contact Info & Emergency */}
<div className="lg:col-span-5 space-y-8">
{/* Emergency Card */}
<SpotlightCard
className="rounded-3xl p-8 border border-red-200/50 bg-red-50/50 dark:bg-red-900/20 dark:border-red-500/30 backdrop-blur-sm"
spotlightColor="rgba(239, 68, 68, 0.15)"
>
<div className="flex items-start space-x-4">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/50 rounded-xl flex items-center justify-center flex-shrink-0 text-red-600 dark:text-red-400">
<ShieldAlert className="w-7 h-7" />
</div>
<div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Bantuan Darurat 24/7</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6 text-sm">
Jika Anda dalam situasi darurat atau bahaya, segera hubungi tim Satgas PPKS melalui saluran khusus ini.
</p>
<FlowButton
text="Hubungi Darurat Sekarang"
href={`https://wa.me/${contact.whatsapp?.replace(/[^0-9]/g, '')}`}
target="_blank"
colorStr="#ef4444"
hoverColorStr="#dc2626"
className="w-full"
/>
</div>
</div>
</SpotlightCard>
{/* General Contact Info */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{contactMethods.map((method, index) => (
<SpotlightCard
key={index}
className="rounded-2xl p-6 border border-white/20 bg-white/60 dark:bg-black/20 dark:border-white/10 backdrop-blur-md shadow-sm hover:shadow-md transition-all group"
spotlightColor={method.spotlight}
>
<a
href={method.href}
target={method.href.startsWith('http') ? '_blank' : '_self'}
rel={method.href.startsWith('http') ? 'noopener noreferrer' : undefined}
className="flex flex-col items-center text-center h-full"
>
<div className={`w-12 h-12 bg-gradient-to-br from-primary/10 to-accent/10 dark:from-primary/20 dark:to-accent/20 rounded-xl flex items-center justify-center ${method.color} flex-shrink-0`}>
<div className={method.color === 'text-primary' ? 'text-primary dark:text-primary-light' : method.color === 'text-accent' ? 'text-accent dark:text-accent-light' : 'text-gray-700 dark:text-gray-300'}>
{method.icon}
</div>
<div className={`w-12 h-12 rounded-full bg-gray-50 dark:bg-white/5 flex items-center justify-center mb-4 ${method.color} group-hover:scale-110 transition-transform duration-300`}>
{method.icon}
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">{method.label}</h4>
<a
href={method.href}
className="text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light transition-colors"
target={method.href.startsWith('http') ? '_blank' : '_self'}
rel={method.href.startsWith('http') ? 'noopener noreferrer' : undefined}
<h4 className="font-semibold text-gray-900 dark:text-white mb-1">{method.label}</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 break-all">{method.value}</p>
</a>
</SpotlightCard>
))}
</div>
{/* Operating Hours */}
<div className="bg-white/60 dark:bg-black/20 border border-white/20 dark:border-white/10 rounded-2xl p-6 backdrop-blur-sm">
<div className="flex items-center space-x-3 mb-4">
<Clock className="w-5 h-5 text-primary" />
<h4 className="font-semibold text-gray-900 dark:text-white">Jam Operasional Kantor</h4>
</div>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-300">
<li className="flex justify-between">
<span>Senin - Jumat</span>
<span className="font-medium">08.00 - 16.00 WIB</span>
</li>
<li className="flex justify-between">
<span>Sabtu - Minggu</span>
<span className="font-medium text-red-500">Tutup (Kecuali Darurat)</span>
</li>
</ul>
</div>
</div>
{/* Right Column: Map */}
<div className="lg:col-span-7 h-full min-h-[500px]">
<SpotlightCard
className="h-full rounded-3xl p-2 border border-white/20 bg-white/60 dark:bg-black/20 dark:border-white/10 backdrop-blur-md shadow-xl overflow-hidden"
spotlightColor="rgba(255, 255, 255, 0.1)"
>
<div className="relative w-full h-full rounded-2xl overflow-hidden group">
<iframe
title="Lokasi Politeknik Negeri Jember"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3949.424564483758!2d113.72093787447761!3d-8.16021369187063!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd695b617d8f623%3A0x280e466373737b60!2sPoliteknik%20Negeri%20Jember!5e0!3m2!1sid!2sid!4v1708150000000!5m2!1sid!2sid"
width="100%"
height="100%"
style={{ border: 0, minHeight: '500px' }}
allowFullScreen=""
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="grayscale hover:grayscale-0 transition-all duration-700 ease-in-out"
></iframe>
{/* Map Overlay Info */}
<div className="absolute bottom-6 left-6 right-6 bg-white/90 dark:bg-black/80 backdrop-blur-md p-4 rounded-xl border border-white/20 shadow-lg translate-y-full opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-500">
<div className="flex items-start space-x-3">
<MapPin className="w-5 h-5 text-red-500 flex-shrink-0 mt-1" />
<div>
<h5 className="font-bold text-gray-900 dark:text-white">Kampus Politeknik Negeri Jember</h5>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1">{contact.address}</p>
<a
href="https://maps.app.goo.gl/politekniknegerijember"
target="_blank"
rel="noopener noreferrer"
className="inline-block mt-2 text-xs font-semibold text-primary hover:underline"
>
{method.value}
Buka di Google Maps
</a>
</div>
</motion.div>
))}
</div>
{/* Address */}
<div className="p-4 bg-gray-50 rounded-xl">
<div className="flex items-start space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-accent/10 rounded-xl flex items-center justify-center text-primary flex-shrink-0">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Alamat</h4>
<p className="text-gray-600 leading-relaxed">{contact.address}</p>
</div>
</div>
</div>
{/* Social Media */}
<div className="mt-8 pt-8 border-t border-gray-200">
<h4 className="font-semibold text-gray-900 mb-4">Ikuti Kami</h4>
<div className="flex space-x-4">
{socialMedia.map((social, index) => (
<motion.a
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className={`w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center text-gray-600 ${social.color} transition-all duration-300`}
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 * index }}
whileHover={{ scale: 1.1 }}
>
{social.icon}
</motion.a>
))}
</div>
</div>
</div>
</motion.div>
{/* Emergency Contact */}
<motion.div
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<div className="bg-gradient-to-br from-danger/10 to-danger/5 border border-danger/20 rounded-3xl p-8">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-danger/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-danger" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-4">Butuh Bantuan Darurat?</h3>
<p className="text-gray-600 leading-relaxed">
Jika Anda atau orang lain berada dalam situasi darurat, segera hubungi kami untuk respons cepat 24/7.
</p>
</div>
<div className="space-y-4">
<motion.a
href={`https://wa.me/${contact.whatsapp?.replace(/[^0-9]/g, '')}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center w-full px-6 py-4 bg-danger text-white rounded-xl hover:bg-danger-dark transition-all duration-300 font-semibold shadow-soft hover:shadow-lg"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.3 }}
whileHover={{ scale: 1.02 }}
>
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.149-.67.149-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
</svg>
Hubungi WhatsApp Darurat
</motion.a>
<motion.div
className="text-center p-4 bg-white/80 rounded-xl"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<div className="flex items-center justify-center space-x-2 text-gray-600">
<svg className="w-5 h-5 text-accent" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
<span className="font-medium">Respons 24/7 - Privasi Terjamin</span>
</div>
</motion.div>
</div>
</div>
</motion.div>
</SpotlightCard>
</div>
</div>
)}
</div>

View File

@ -1,237 +1,135 @@
import React from 'react';
import { motion } from 'framer-motion';
import { fadeIn } from '../utils/motionVariants';
import { Facebook, Instagram, Twitter, Linkedin, Heart, Shield, Mail, Phone, MapPin } from 'lucide-react';
const Footer = () => {
const currentYear = new Date().getFullYear();
const quickLinks = [
{ name: 'Beranda', href: '#hero' },
{ name: 'Tentang Kami', href: '#about' },
{ name: 'Layanan', href: '#services' },
{ name: 'Artikel', href: '/artikel' },
{ name: 'Kontak', href: '#contact' }
];
const services = [
{ name: 'WhatsApp Reporting', href: 'https://wa.me/6281234567890' },
{ name: 'Form Reporting', href: '/artikel' },
{ name: 'Konsultasi', href: '#contact' },
{ name: 'Pendampingan', href: '#contact' }
];
const footerLinks = {
'Menu Utama': [
{ name: 'Beranda', href: '#hero' },
{ name: 'Tentang Kami', href: '#about' },
{ name: 'Layanan', href: '#services' },
{ name: 'Artikel', href: '/artikel' },
],
'Layanan': [
{ name: 'Pelaporan Online', href: '#services' },
{ name: 'Konsultasi Psikologi', href: '#contact' },
{ name: 'Pendampingan Hukum', href: '#contact' },
{ name: 'Edukasi & Sosialisasi', href: '/artikel' },
],
'Bantuan': [
{ name: 'Kontak Darurat', href: '#contact' },
{ name: 'Prosedur Lapor', href: '#services' },
{ name: 'FAQ', href: '#faq' },
{ name: 'Kebijakan Privasi', href: '#' },
]
};
const socialLinks = [
{
name: 'Instagram',
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zM5.838 12a6.162 6.162 0 1112.324 0 6.162 6.162 0 01-12.324 0zM12 16a4 4 0 110-8 4 4 0 010 8zm4.965-10.405a1.44 1.44 0 112.881.001 1.44 1.44 0 01-2.881-.001z"/>
</svg>
),
href: 'https://instagram.com/satgasppkpt_polije'
}
{ icon: <Instagram className="w-5 h-5" />, href: 'https://instagram.com/satgasppkpt_polije', label: 'Instagram' },
{ icon: <Twitter className="w-5 h-5" />, href: '#', label: 'Twitter' },
{ icon: <Facebook className="w-5 h-5" />, href: '#', label: 'Facebook' },
{ icon: <Linkedin className="w-5 h-5" />, href: '#', label: 'LinkedIn' },
];
return (
<footer className="bg-gray-900 text-white relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 right-10 w-64 h-64 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-10 left-10 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
</div>
<footer className="bg-white/80 dark:bg-black/80 backdrop-blur-md border-t border-gray-200/50 dark:border-white/5 pt-20 pb-10 relative overflow-hidden">
{/* Background Gradients */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-primary/5 via-transparent to-transparent opacity-70"></div>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,_var(--tw-gradient-stops))] from-blue-500/5 via-transparent to-transparent opacity-70"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Main Footer Content */}
<div className="py-16">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
{/* Logo and Description */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8 mb-16">
{/* Brand Column */}
<div className="lg:col-span-4 space-y-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="flex items-center gap-4"
>
<div className="flex items-center space-x-3">
<div className="relative">
<div className="w-12 h-12 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center shadow-lg">
<div className="w-7 h-7 bg-white rounded-full flex items-center justify-center">
<div className="w-4 h-4 bg-gradient-to-br from-primary to-accent rounded-full"></div>
</div>
</div>
<div className="absolute -top-1 -right-1 w-4 h-4 bg-accent rounded-full animate-pulse"></div>
</div>
<span className="text-2xl font-bold text-white">PolijeCare</span>
</div>
<p className="text-gray-300 leading-relaxed">
Satgas PPKPT Politeknik Negeri Jember - Layanan pengaduan dan pendampingan terpercaya bagi sivitas akademika.
</p>
{/* Social Links */}
<div className="flex space-x-4">
{socialLinks.map((social, index) => (
<motion.a
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 bg-gray-800 rounded-xl flex items-center justify-center text-gray-400 hover:text-accent transition-all duration-300"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.1, y: -2 }}
>
{social.icon}
</motion.a>
))}
<img
src="/logo_polijecare.png"
alt="PolijeCare Logo"
className="w-14 h-14 object-contain filter drop-shadow-sm"
/>
<div className="space-y-0.5">
<h3 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
PolijeCare
</h3>
<p className="text-[10px] uppercase tracking-[0.2em] font-semibold text-primary">
Satgas PPKS Polije
</p>
</div>
</motion.div>
{/* Quick Links */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 }}
>
<h3 className="text-lg font-bold text-white">Quick Links</h3>
<ul className="space-y-3">
{quickLinks.map((link, index) => (
<motion.li
key={index}
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
>
<a
href={link.href}
className="text-gray-300 hover:text-accent transition-colors duration-300 flex items-center group"
>
<svg className="w-4 h-4 mr-2 transform group-hover:translate-x-1 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
{link.name}
</a>
</motion.li>
))}
</ul>
</motion.div>
<p className="text-gray-500 dark:text-gray-400 leading-relaxed max-w-sm text-sm">
Mewujudkan lingkungan kampus yang aman, inklusif, dan bebas kekerasan seksual. Kami hadir untuk melayani, melindungi, dan mendampingi sivitas akademika Polije.
</p>
{/* Services */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<h3 className="text-lg font-bold text-white">Layanan</h3>
<ul className="space-y-3">
{services.map((service, index) => (
<motion.li
key={index}
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
>
<a
href={service.href}
className="text-gray-300 hover:text-accent transition-colors duration-300 flex items-center group"
target={service.href.startsWith('http') ? '_blank' : '_self'}
rel={service.href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
<svg className="w-4 h-4 mr-2 transform group-hover:translate-x-1 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
{service.name}
</a>
</motion.li>
))}
</ul>
</motion.div>
{/* Contact Info */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<h3 className="text-lg font-bold text-white">Kontak Darurat</h3>
<div className="space-y-4">
<div className="flex gap-3">
{socialLinks.map((social, index) => (
<motion.a
href="https://wa.me/6281234567890"
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-3 text-gray-300 hover:text-accent transition-colors"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
whileHover={{ x: 4 }}
aria-label={social.label}
className="w-9 h-9 flex items-center justify-center rounded-full bg-gray-100 dark:bg-white/5 text-gray-500 dark:text-gray-400 hover:bg-primary hover:text-white dark:hover:bg-primary dark:hover:text-white transition-all duration-300"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<div className="w-10 h-10 bg-green-500/20 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.149-.67.149-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
</svg>
</div>
<div>
<div className="font-medium">WhatsApp Darurat</div>
<div className="text-sm text-gray-400">24/7 Available</div>
</div>
{React.cloneElement(social.icon, { className: "w-4 h-4" })}
</motion.a>
))}
</div>
</div>
<motion.div
className="flex items-center space-x-3 text-gray-300"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<div className="w-10 h-10 bg-primary/20 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-primary" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</div>
<div>
<div className="font-medium">Alamat</div>
<div className="text-sm text-gray-400">Jl. Mastrip PO Box 164, Jember</div>
</div>
</motion.div>
{/* Links Columns */}
<div className="lg:col-span-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12">
{Object.entries(footerLinks).map(([title, links], columnIndex) => (
<div key={title} className="space-y-6">
<h4 className="font-semibold text-gray-900 dark:text-white text-sm uppercase tracking-wider">{title}</h4>
<ul className="space-y-3">
{links.map((link, index) => (
<motion.li
key={index}
initial={{ opacity: 0, x: -10 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.1 + (index * 0.05) }}
>
<a
href={link.href}
className="text-sm text-gray-500 dark:text-gray-400 hover:text-primary dark:hover:text-primary transition-colors inline-flex items-center group"
>
<span className="relative overflow-hidden">
{link.name}
<span className="absolute bottom-0 left-0 w-full h-[1px] bg-primary transform -translate-x-full group-hover:translate-x-0 transition-transform duration-300"></span>
</span>
</a>
</motion.li>
))}
</ul>
</div>
</motion.div>
))}
</div>
</div>
{/* Bottom Bar */}
<div className="py-6 border-t border-gray-800">
<motion.div
className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<div className="text-gray-400 text-sm">
© {currentYear} PolijeCare. All rights reserved.
<div className="pt-8 border-t border-gray-200/50 dark:border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-xs text-gray-500 dark:text-gray-500">
<p>© {currentYear} PolijeCare. All rights reserved.</p>
<div className="flex gap-8">
<div className="flex items-center gap-2">
<MapPin className="w-3 h-3" />
<span>Jember, East Java</span>
</div>
<div className="flex items-center space-x-6 text-gray-400 text-sm">
<a href="#" className="hover:text-accent transition-colors">Privacy Policy</a>
<a href="#" className="hover:text-accent transition-colors">Terms of Service</a>
<a href="#" className="hover:text-accent transition-colors">Cookie Policy</a>
<div className="flex items-center gap-2">
<Mail className="w-3 h-3" />
<span>satgasppkpt@polije.ac.id</span>
</div>
</motion.div>
</div>
</div>
</div>
</footer>

View File

@ -1,9 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { Component as ReportButton } from './button';
import { Icon } from '@iconify/react';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, slideLeft, slideRight } from '../utils/motionVariants';
const Hero = ({ heroData }) => {
const navigate = useNavigate();
const defaultHero = {
title: 'Aman Bicara, Aman Melapor',
subtitle: 'Satgas PPKPT Politeknik Negeri Jember',
@ -13,32 +16,32 @@ const Hero = ({ heroData }) => {
const hero = heroData || defaultHero;
return (
<section
id="hero"
className="min-h-screen flex items-center bg-gradient-to-br from-gray-50 to-purple-50 dark:from-gray-900 dark:to-purple-900 relative overflow-hidden pt-16 transition-colors duration-300"
<section
id="hero"
className="min-h-screen flex items-center bg-soft-white relative overflow-hidden pt-16 transition-colors duration-300"
>
{/* Background Decorations */}
<div className="absolute inset-0 overflow-hidden">
<motion.div
className="absolute top-20 left-10 w-72 h-72 bg-accent/10 rounded-full blur-3xl"
animate={{
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute top-[-10%] left-[-5%] w-[500px] h-[500px] bg-primary-light/30 rounded-full blur-[100px]"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3]
opacity: [0.3, 0.5, 0.3]
}}
transition={{
duration: 8,
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut"
ease: "easeInOut"
}}
/>
<motion.div
className="absolute bottom-20 right-10 w-96 h-96 bg-primary/10 rounded-full blur-3xl"
animate={{
<motion.div
className="absolute bottom-[-10%] right-[-5%] w-[600px] h-[600px] bg-primary/10 rounded-full blur-[120px]"
animate={{
scale: [1, 1.3, 1],
opacity: [0.3, 0.4, 0.3]
opacity: [0.3, 0.4, 0.3]
}}
transition={{
duration: 10,
transition={{
duration: 10,
repeat: Infinity,
ease: "easeInOut",
delay: 2
@ -46,25 +49,25 @@ const Hero = ({ heroData }) => {
/>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div className="max-w-[1440px] mx-auto px-8 sm:px-12 lg:px-16 relative z-10">
<div className="grid lg:grid-cols-2 gap-8 lg:gap-12 items-center">
{/* Left Content */}
<motion.div
className="space-y-8"
<motion.div
className="space-y-6"
variants={slideUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.8 }}
>
<motion.div
<motion.div
className="space-y-4"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.2 }}
>
<motion.h1
className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-tight"
<motion.h1
className="text-5xl md:text-6xl lg:text-7xl font-bold text-gray-900 dark:text-white leading-tight"
variants={slideUp}
initial="hidden"
animate="visible"
@ -72,9 +75,9 @@ const Hero = ({ heroData }) => {
>
{hero.title}
</motion.h1>
<motion.h2
className="text-xl md:text-2xl lg:text-3xl font-semibold text-primary dark:text-primary-light"
<motion.h2
className="text-2xl md:text-3xl lg:text-4xl font-semibold text-[#191970] dark:text-blue-300"
variants={slideUp}
initial="hidden"
animate="visible"
@ -84,28 +87,35 @@ const Hero = ({ heroData }) => {
</motion.h2>
</motion.div>
<motion.p
className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed max-w-xl"
<motion.p
className="text-xl text-gray-600 dark:text-gray-300 leading-relaxed max-w-2xl"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.5 }}
>
{hero.description}
{hero.description ? (
hero.description
) : (
<>
Kami siap mendengar dan membantu Anda dengan{' '}
<span className="highlight-marker" style={{ '--delay': '1' }}>profesionalisme</span> dan{' '}
<span className="highlight-marker" style={{ '--delay': '1.6' }}>kerahasiaan terjamin</span>.{' '}
Setiap laporan akan ditangani dengan{' '}
<span className="highlight-marker" style={{ '--delay': '2.2' }}>empati</span> dan{' '}
<span className="highlight-marker" style={{ '--delay': '2.6' }}>seksama</span>.
</>
)}
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 pt-4"
<motion.div
className="flex flex-row gap-4 pt-4 items-start"
variants={slideUp}
initial="hidden"
animate="visible"
transition={{ delay: 0.6 }}
>
<motion.a
href="https://wa.me/6281234567890"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-4 bg-danger text-white rounded-xl hover:bg-danger-dark transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold text-center shadow-soft"
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
@ -113,9 +123,17 @@ const Hero = ({ heroData }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Butuh Bantuan Darurat
</motion.a>
<ReportButton
icon={<Icon icon="solar:phone-calling-bold-duotone" />}
title="Butuh Bantuan Darurat"
size="sm"
className="rounded-full bg-red-600 hover:bg-red-700 border-0"
gradientLight={{ from: "from-red-600", via: "via-red-600", to: "to-red-600" }}
gradientDark={{ from: "from-red-600", via: "via-red-600", to: "to-red-600" }}
onClick={() => window.open('https://wa.me/6281234567890', '_blank')}
/>
</motion.div>
<motion.div
variants={fadeIn}
initial="hidden"
@ -124,42 +142,24 @@ const Hero = ({ heroData }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Link
to="/artikel"
className="px-8 py-4 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold text-center shadow-soft inline-block w-full sm:w-auto"
>
Buat Laporan
</Link>
<ReportButton
icon={<Icon icon="solar:document-add-bold-duotone" />}
title="Buat Laporan"
size="sm"
className="rounded-full bg-[#191970] hover:bg-blue-900 border-0"
gradientLight={{ from: "from-[#191970]", via: "via-[#191970]", to: "to-[#191970]" }}
gradientDark={{ from: "from-[#191970]", via: "via-[#191970]", to: "to-[#191970]" }}
onClick={() => navigate('/artikel')}
/>
</motion.div>
</motion.div>
{/* Trust Indicators */}
<motion.div
className="flex flex-wrap gap-6 pt-8"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.9 }}
>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-accent rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">100% Rahasia</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-accent rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">Profesional</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-accent rounded-full"></div>
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">24/7 Support</span>
</div>
</motion.div>
</motion.div>
{/* Right Content - Logo & Branding */}
<motion.div
<motion.div
className="relative lg:pl-12"
variants={slideRight}
initial="hidden"
@ -167,82 +167,26 @@ const Hero = ({ heroData }) => {
transition={{ duration: 0.8, delay: 0.3 }}
>
{/* Main Logo Container */}
<motion.div
className="relative z-10"
animate={{
y: [0, -20, 0],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut"
}}
<motion.div
className="relative z-10 flex justify-end lg:pr-4"
>
<div className="bg-gradient-to-br from-primary/10 to-accent/10 dark:from-primary/20 dark:to-accent/20 rounded-3xl p-12 backdrop-blur-sm border border-white/50 dark:border-gray-700/50 shadow-soft">
<div className="aspect-square max-w-md mx-auto flex flex-col items-center justify-center space-y-6">
{/* Logo Image */}
<motion.div
animate={{
scale: [1, 1.05, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
className="relative"
>
<div className="w-48 h-48 bg-white dark:bg-gray-800 rounded-2xl shadow-lg flex items-center justify-center p-6">
<img
src="/logo_polijecare.png"
alt="Polijecare Logo"
className="w-full h-full object-contain"
/>
</div>
{/* Glow Effect */}
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 rounded-2xl blur-xl -z-10"></div>
</motion.div>
{/* Brand Text */}
<motion.div
className="text-center space-y-2"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.5 }}
>
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">Polijecare</h3>
<p className="text-primary dark:text-primary-light font-medium">Satgas PPKPT Polije</p>
<div className="w-16 h-1 bg-gradient-to-r from-primary to-accent rounded-full mx-auto"></div>
</motion.div>
</div>
</div>
<img
src="/gambar_header.png"
alt="header gambar"
className="w-full max-w-[550px] h-auto object-cover"
/>
</motion.div>
{/* Background Shape */}
<div className="absolute inset-0 bg-gradient-to-br from-purple-100/50 dark:from-purple-900/20 to-accent/10 dark:to-accent/20 rounded-3xl blur-2xl -z-10"></div>
{/* Background Shape */}
<div className="absolute inset-0 bg-gradient-to-br from-purple-100/50 dark:from-purple-900/20 to-accent/10 dark:to-accent/20 rounded-3xl blur-2xl -z-10"></div>
</motion.div>
</div>
{/* Brand Stats Banner - Glassmorphic Light Design */}
</div>
{/* Scroll Indicator */}
<motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.5, duration: 0.8 }}
>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="flex flex-col items-center space-y-2"
>
<span className="text-sm text-gray-400 dark:text-gray-500 font-medium">Scroll ke bawah</span>
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
<div className="w-1 h-3 bg-gray-600 dark:bg-gray-400 rounded-full mt-2"></div>
</div>
</motion.div>
</motion.div>
</section>
);
};

View File

@ -0,0 +1,373 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FiMail, FiLock, FiAlertCircle, FiEye, FiEyeOff, FiX } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
// Motion variants for animations
const fadeIn = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { duration: 0.3 } }
};
const slideUp = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0, transition: { duration: 0.3, delay: 0.1 } }
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.95 },
show: {
opacity: 1,
scale: 1,
transition: {
duration: 0.2,
type: "spring",
stiffness: 300,
damping: 30
}
},
exit: {
opacity: 0,
scale: 0.95,
transition: { duration: 0.2 }
}
};
const backdropVariants = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { duration: 0.2 } },
exit: { opacity: 0, transition: { duration: 0.2 } }
};
const LoginModal = ({ isOpen, onClose }) => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [loginError, setLoginError] = useState('');
const navigate = useNavigate();
const { login } = useAuth();
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@polije\.ac\.id$/;
return emailRegex.test(email);
};
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email wajib diisi';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'Email harus menggunakan domain @polije.ac.id';
}
if (!formData.password) {
newErrors.password = 'Password wajib diisi';
} else if (formData.password.length < 6) {
newErrors.password = 'Password minimal 6 karakter';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
// Clear login error when user starts typing
if (loginError) {
setLoginError('');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsLoading(true);
setLoginError('');
try {
const result = await login({
email: formData.email,
password: formData.password
});
console.log('🔍 Login result:', result);
if (result && result.success) {
console.log('✅ Login successful');
onClose(); // Close modal
navigate('/redirect');
} else {
console.error('❌ Login failed:', result);
const errorMessage = result?.message || result?.error || 'Login gagal. Silakan coba lagi.';
setLoginError(errorMessage);
}
} catch (error) {
console.error('❌ Login error:', error);
console.error('Error details:', error.response?.data || error.message);
const errorMessage = error.response?.data?.message || error.message || 'Terjadi kesalahan. Silakan coba lagi.';
setLoginError(errorMessage);
}
setIsLoading(false);
};
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const handleClose = () => {
if (!isLoading) {
onClose();
// Reset form
setFormData({ email: '', password: '' });
setErrors({});
setLoginError('');
setShowPassword(false);
}
};
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
handleClose();
}
};
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
handleClose();
}
};
React.useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
} else {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset';
}
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset';
};
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
variants={backdropVariants}
initial="hidden"
animate="show"
exit="exit"
onClick={handleBackdropClick}
>
{/* Backdrop */}
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm"></div>
{/* Modal */}
<motion.div
className="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden"
variants={modalVariants}
initial="hidden"
animate="show"
exit="exit"
onClick={(e) => e.stopPropagation()}
>
{/* Close Button */}
<button
onClick={handleClose}
disabled={isLoading}
className="absolute top-4 right-4 z-10 p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<FiX className="w-5 h-5" />
</button>
{/* Modal Content */}
<div className="p-6 sm:p-8">
{/* Header with Protection Icon */}
<motion.div
className="text-center mb-6"
variants={slideUp}
>
<div className="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-full flex items-center justify-center mx-auto mb-4 shadow-lg">
<img
src="/logo_polijecare.png"
alt="Polijecare Logo"
className="w-10 h-10 object-contain"
/>
</div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
PolijeCare
</h1>
<p className="text-sm text-gray-600 dark:text-gray-300">
Portal Layanan Perlindungan & Kesejahteraan
</p>
</motion.div>
{/* Login Form */}
<motion.form
onSubmit={handleSubmit}
variants={slideUp}
>
<div className="mb-6">
<p className="text-sm text-gray-600 dark:text-gray-300 text-center leading-relaxed">
Masuk menggunakan akun resmi Polije<br />
untuk menjaga keamanan dan kerahasiaan data Anda.
</p>
</div>
{/* Login Error Alert */}
{loginError && (
<motion.div
className="mb-6 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3 text-red-600 dark:text-red-400 text-sm"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<FiAlertCircle className="w-5 h-5 flex-shrink-0" />
<span>{loginError}</span>
</motion.div>
)}
{/* Email Field */}
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Polije
</label>
<div className="relative">
<FiMail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
errors.email ? 'border-red-300 dark:border-red-600' : 'border-gray-300'
}`}
placeholder="nama@polije.ac.id"
disabled={isLoading}
autoComplete="email"
/>
</div>
{errors.email && (
<motion.p
className="mt-1 text-sm text-red-600 dark:text-red-400"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{errors.email}
</motion.p>
)}
</div>
{/* Password Field */}
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Password
</label>
<div className="relative">
<FiLock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type={showPassword ? 'text' : 'password'}
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className={`w-full pl-10 pr-12 py-3 border rounded-lg focus:ring-2 focus:ring-primary/20 focus:border-primary transition-colors dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
errors.password ? 'border-red-300 dark:border-red-600' : 'border-gray-300'
}`}
placeholder="Masukkan password"
disabled={isLoading}
autoComplete="current-password"
/>
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors disabled:opacity-50"
onClick={togglePasswordVisibility}
disabled={isLoading}
>
{showPassword ? <FiEyeOff className="w-5 h-5" /> : <FiEye className="w-5 h-5" />}
</button>
</div>
{errors.password && (
<motion.p
className="mt-1 text-sm text-red-600 dark:text-red-400"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{errors.password}
</motion.p>
)}
</div>
{/* Submit Button */}
<motion.button
type="submit"
className="w-full py-3 px-4 bg-primary text-white font-medium rounded-lg shadow-lg hover:bg-primary-dark hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none"
disabled={isLoading}
whileHover={{ scale: isLoading ? 1 : 1.02 }}
whileTap={{ scale: isLoading ? 1 : 0.98 }}
>
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<span>Masuk...</span>
</div>
) : (
'Masuk'
)}
</motion.button>
</motion.form>
{/* Footer */}
<motion.div
className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"
variants={slideUp}
>
<p className="text-xs text-gray-500 dark:text-gray-400 text-center leading-relaxed">
Dilindungi dengan enkripsi end-to-end<br />
© 2026 PolijeCare - Politeknik Negeri Jember
</p>
</motion.div>
</div>
{/* Background Decoration */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute -top-20 -right-20 w-40 h-40 bg-gradient-to-br from-primary/10 to-accent/5 rounded-full blur-2xl"></div>
<div className="absolute -bottom-16 -left-16 w-32 h-32 bg-gradient-to-br from-accent/10 to-primary/5 rounded-full blur-xl"></div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};
export default LoginModal;

View File

@ -1,13 +1,19 @@
import React, { useState, useEffect } from 'react';
import { Home, Info, FileText, BookOpen, Phone } from 'lucide-react';
import { ExpandableTabs } from "@/components/ui/expandable-tabs";
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '../hooks/useAuth';
import { fadeIn, slideDown } from '../utils/motionVariants';
import ThemeToggle from './ThemeToggle';
import Switch from './sky-toggle';
import LoginModal from './LoginModal';
const Navbar = () => {
const [isScrolled, setIsScrolled] = useState(false);
const [activeLink, setActiveLink] = useState('#hero');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
const { isAuthenticated, user, logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
@ -20,14 +26,62 @@ const Navbar = () => {
{ name: 'Kontak', href: '#contact' }
];
const tabs = [
{ title: "Beranda", icon: Home },
{ title: "Tentang Kami", icon: Info },
{ title: "Cara Melapor", icon: FileText },
{ title: "Artikel", icon: BookOpen },
{ title: "Kontak", icon: Phone },
];
const handleNavClick = (href) => {
if (href.startsWith('#')) {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
setIsMobileMenuOpen(false);
};
useEffect(() => {
let ticking = false;
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (!ticking) {
requestAnimationFrame(() => {
// Logic for switching navbar type (Standard vs Expandable)
const aboutSection = document.getElementById('about');
const threshold = aboutSection ? aboutSection.offsetTop - 400 : window.innerHeight - 200;
setIsScrolled(window.scrollY > threshold);
// ScrollSpy Logic
const sections = navLinks.map(link => link.href.substring(1));
let currentSection = "";
for (const section of sections) {
const element = document.getElementById(section);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top <= 150 && rect.bottom >= 150) {
currentSection = "#" + section;
}
}
}
if (currentSection && currentSection !== activeLink) {
setActiveLink(currentSection);
}
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
}, [navLinks, activeLink]);
const handleLogout = () => {
logout();
@ -36,7 +90,7 @@ const Navbar = () => {
const handleDashboardRedirect = () => {
if (!user) return;
switch (user.role) {
case 'user':
navigate('/user/dashboard');
@ -53,226 +107,249 @@ const Navbar = () => {
}
};
const handleNavClick = (href) => {
if (href.startsWith('#')) {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
setIsMobileMenuOpen(false);
};
return (
<>
<motion.nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled
? 'bg-white/95 backdrop-blur-md shadow-soft border-b border-gray-100'
: 'bg-white shadow-sm'
}`}
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link
to="/"
className="flex items-center space-x-3 group"
onClick={() => handleNavClick('#hero')}
>
<div className="relative">
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-soft group-hover:shadow-card transition-all duration-300 border border-primary/20">
<img
src="/logo_polijecare.png"
alt="PolijeCare Logo"
className="w-10 h-10 object-contain"
/>
</div>
<div className="absolute -top-1 -right-1 w-3 h-3 bg-accent rounded-full animate-pulse"></div>
</div>
<span className="text-xl font-bold text-gray-900 group-hover:text-primary transition-colors">
Polijecare
</span>
</Link>
{/* Desktop Navigation */}
<div className="hidden md:flex items-center space-x-8">
<motion.div
className="flex space-x-6"
variants={slideDown}
initial="hidden"
animate="visible"
>
{navLinks.map((link, index) => (
<motion.div
key={link.name}
variants={fadeIn}
transition={{ delay: 0.1 * index }}
>
{link.href.startsWith('#') ? (
<button
onClick={() => handleNavClick(link.href)}
className="text-gray-600 hover:text-primary font-medium transition-colors relative group"
>
{link.name}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</button>
) : (
<Link
to={link.href}
className="text-gray-600 hover:text-primary font-medium transition-colors relative group"
>
{link.name}
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
)}
</motion.div>
))}
</motion.div>
{/* Auth Buttons & Theme Toggle */}
<div className="flex items-center space-x-4">
{isAuthenticated ? (
<>
<button
onClick={handleDashboardRedirect}
className="px-4 py-2 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-soft font-medium"
>
Dashboard
</button>
<button
onClick={handleLogout}
className="px-4 py-2 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
>
Keluar
</button>
</>
) : (
<Link
to="/login"
className="px-6 py-2.5 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-soft font-medium"
>
Masuk
</Link>
)}
{/* Theme Toggle */}
<ThemeToggle />
</div>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="text-gray-600 hover:text-primary p-2 rounded-lg hover:bg-gray-50 transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<AnimatePresence mode="wait">
{isMobileMenuOpen ? (
<motion.path
key="close"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.2 }}
/>
) : (
<motion.path
key="menu"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
</svg>
</button>
</div>
</div>
</div>
{/* Mobile menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
className="md:hidden absolute top-full left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<div className="px-4 py-6 space-y-4">
{navLinks.map((link, index) => (
<motion.div
key={link.name}
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.05 * index }}
>
{link.href.startsWith('#') ? (
<button
onClick={() => handleNavClick(link.href)}
className="block w-full text-left px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium"
>
{link.name}
</button>
) : (
<Link
to={link.href}
onClick={() => setIsMobileMenuOpen(false)}
className="block w-full px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium"
>
{link.name}
</Link>
)}
</motion.div>
))}
<div className="pt-4 border-t border-gray-100 dark:border-gray-700 space-y-3">
<AnimatePresence mode="wait">
{isScrolled ? (
<motion.div
key="expandable-tabs"
initial={{ opacity: 0, y: -10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: -10, x: "-50%", transition: { duration: 0.15 } }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="fixed top-4 left-1/2 z-50 transform -translate-x-1/2 will-change-transform"
>
<ExpandableTabs
tabs={tabs}
activeTab={navLinks.findIndex(link => link.href === activeLink)}
onChange={(index) => {
if (index !== null) {
const href = navLinks[index].href;
setActiveLink(href);
handleNavClick(href);
}
}}
trailingElement={
<div className="flex items-center gap-2">
<Switch />
{isAuthenticated ? (
<>
<button
onClick={handleDashboardRedirect}
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
className="px-5 py-2.5 text-sm font-medium bg-primary text-white rounded-full hover:bg-primary-dark transition-all duration-300 hover:shadow-lg"
>
Dashboard
</button>
<button
onClick={handleLogout}
className="w-full px-4 py-3 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
className="px-5 py-2.5 text-sm font-medium border-2 border-primary text-primary rounded-full hover:bg-primary hover:text-white transition-all duration-300"
>
Keluar
</button>
</>
) : (
<Link
to="/login"
onClick={() => setIsMobileMenuOpen(false)}
className="block w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium text-center"
<button
onClick={() => setIsLoginModalOpen(true)}
className="px-6 py-2.5 text-sm font-medium bg-[#191970] text-white rounded-full hover:bg-blue-900 transition-all duration-300 hover:shadow-lg"
>
Masuk
</Link>
</button>
)}
</div>
}
/>
</motion.div>
) : (
<motion.nav
key="navbar"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10, transition: { duration: 0.15 } }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="fixed top-0 left-0 right-0 z-50 transition-colors duration-300 bg-white/50 backdrop-blur-md border-b border-white/20 shadow-sm will-change-transform"
>
<div className="w-full px-8 lg:px-12">
<div className="flex items-center justify-between h-20">
{/* Logo Section - Left */}
<Link to="/" className="flex items-center space-x-2 cursor-default">
<img
src="/logo_polije.png"
alt="Logo Polije"
className="h-12 w-auto object-contain"
/>
<img
src="/logo_polijecare.png"
alt="Polijecare Logo"
className="h-12 w-auto object-contain"
/>
</Link>
{/* Centered Navigation Links */}
<div className="hidden md:flex items-center space-x-1">
{navLinks.map((link) => (
<button
key={link.name}
onClick={() => {
setActiveLink(link.href);
handleNavClick(link.href);
}}
className={`px-5 py-2.5 text-base font-medium rounded-full transition-all duration-200 ${activeLink === link.href
? 'bg-[#191970] text-white shadow-[0_4px_15px_rgba(25,25,112,0.4)]'
: 'text-gray-600 hover:text-[#191970] hover:bg-gray-100'
}`}
>
{link.name}
</button>
))}
</div>
{/* Right Section - Auth & Theme */}
<div className="hidden md:flex items-center space-x-4">
<Switch />
{isAuthenticated ? (
<>
<button
onClick={handleDashboardRedirect}
className="px-5 py-2.5 bg-primary text-white rounded-full hover:bg-primary-dark transition-all duration-300 hover:shadow-lg font-medium text-sm"
>
Dashboard
</button>
<button
onClick={handleLogout}
className="px-5 py-2.5 border-2 border-primary text-primary rounded-full hover:bg-primary hover:text-white transition-all duration-300 font-medium text-sm"
>
Keluar
</button>
</>
) : (
<button
onClick={() => setIsLoginModalOpen(true)}
className="px-6 py-2.5 bg-[#191970] text-white rounded-full hover:bg-blue-900 transition-all duration-300 hover:shadow-lg font-medium text-sm"
>
Masuk
</button>
)}
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="text-gray-600 hover:text-primary p-2 rounded-lg hover:bg-gray-50 transition-colors"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<AnimatePresence mode="wait">
{isMobileMenuOpen ? (
<motion.path
key="close"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.2 }}
/>
) : (
<motion.path
key="menu"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</AnimatePresence>
</svg>
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.nav>
</div>
{/* Mobile menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
className="md:hidden absolute top-full left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<div className="px-4 py-6 space-y-4">
{navLinks.map((link, index) => (
<motion.div
key={link.name}
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.05 * index }}
>
{link.href.startsWith('#') ? (
<button
onClick={() => handleNavClick(link.href)}
className="block w-full text-left px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium"
>
{link.name}
</button>
) : (
<Link
to={link.href}
onClick={() => setIsMobileMenuOpen(false)}
className="block w-full px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium"
>
{link.name}
</Link>
)}
</motion.div>
))}
<div className="pt-4 border-t border-gray-100 dark:border-gray-700 space-y-3">
{isAuthenticated ? (
<>
<button
onClick={handleDashboardRedirect}
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
>
Dashboard
</button>
<button
onClick={handleLogout}
className="w-full px-4 py-3 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
>
Keluar
</button>
</>
) : (
<button
onClick={() => {
setIsLoginModalOpen(true);
setIsMobileMenuOpen(false);
}}
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
>
Masuk
</button>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.nav>
)}
</AnimatePresence>
<LoginModal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
/>
</>
);
};

View File

@ -0,0 +1,47 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const ProtectedRoute = ({ children, requiredRole }) => {
const { user, isAuthenticated, isLoading } = useAuth();
// Show loading while checking auth
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
// Redirect to login if not authenticated
if (!isAuthenticated) {
return <Navigate to="/" replace />;
}
// Check role if specified
if (requiredRole && user?.role !== requiredRole) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="text-6xl text-red-500 mb-4">🚫</div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Akses Ditolak</h1>
<p className="text-gray-600 mb-6">
Anda tidak memiliki izin untuk mengakses halaman ini.
</p>
<button
onClick={() => window.history.back()}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
Kembali
</button>
</div>
</div>
);
}
// Render children if authenticated and authorized
return children;
};
export default ProtectedRoute;

View File

@ -2,99 +2,119 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
import {
MessageCircle,
FileText,
Shield,
Users,
HeartHandshake,
Phone,
CheckCircle2,
Search,
Gavel,
Smile
} from 'lucide-react';
import { SpotlightCard } from './ui/spotlight-card';
import { FlowButton } from './ui/flow-button';
const Services = () => {
const reportingMethods = [
{
id: 'whatsapp',
title: 'Via WhatsApp',
description: 'Laporkan secara langsung melalui WhatsApp untuk respons cepat dan konsultasi awal dengan tim kami.',
icon: (
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.149-.67.149-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
</svg>
),
features: ['Respons 24/7', 'Konsultasi awal', 'Bimbingan langkah selanjutnya'],
buttonText: 'Butuh Bantuan Darurat',
buttonColor: 'bg-accent hover:bg-accent-dark',
buttonLink: 'https://wa.me/6281234567890'
description: 'Layanan cepat tanggap untuk konsultasi awal dan pelaporan darurat. Terhubung langsung dengan tim satgas kami.',
icon: <MessageCircle className="w-8 h-8" />,
features: ['Respons 24/7', 'Konsultasi Privat', 'Pendampingan Awal'],
buttonText: 'Chat WhatsApp Sekarang',
buttonColor: 'bg-green-600 hover:bg-green-700',
buttonLink: 'https://wa.me/6281234567890',
gradient: 'from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20',
iconColor: 'text-green-600 dark:text-green-400',
spotlightColor: 'rgba(34, 197, 94, 0.2)' // Green glow for WhatsApp
},
{
id: 'form',
title: 'Form Pengaduan Online',
description: 'Isi form pengaduan secara online dengan detail lengkap dan upload bukti pendukung untuk proses yang lebih terstruktur.',
icon: (
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
<path d="M8 12h8v2H8zm0 4h8v2H8zm0-8h5v2H8z"/>
</svg>
),
features: ['Form terstruktur', 'Upload bukti', 'Tracking status laporan'],
buttonText: 'Laporkan Sekarang',
buttonColor: 'bg-primary hover:bg-primary-dark',
buttonLink: '/artikel'
title: 'Form Pengaduan',
description: 'Saluran resmi untuk pelaporan mendetail. Mendukung lampiran bukti dan kronologi lengkap untuk investigasi.',
icon: <FileText className="w-8 h-8" />,
features: ['Form Terstruktur', 'Upload Bukti Aman', 'Tracking Status'],
buttonText: 'Isi Form Laporan',
buttonColor: 'bg-[#191970] hover:bg-blue-900',
buttonLink: '/artikel',
gradient: 'from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20',
iconColor: 'text-[#191970] dark:text-blue-400',
spotlightColor: 'rgba(139, 92, 246, 0.2)' // Purple glow for Form
}
];
const handlingFlow = [
{
step: '01',
title: 'Pelaporan',
desc: 'Laporan masuk via WA atau Website.',
icon: <FileText className="w-5 h-5" />
},
{
step: '02',
title: 'Verifikasi',
desc: 'Validasi data oleh tim Satgas.',
icon: <CheckCircle2 className="w-5 h-5" />
},
{
step: '03',
title: 'Tindak Lanjut',
desc: 'Investigasi atau mediasi kasus.',
icon: <Search className="w-5 h-5" />
},
{
step: '04',
title: 'Penyelesaian',
desc: 'Pemulihan dan penutupan kasus.',
icon: <Smile className="w-5 h-5" />
}
];
const importantInfo = [
{
icon: '🛡️',
title: 'Aman',
description: 'Identitas Anda akan dirahasiakan sepenuhnya'
icon: <Shield className="w-6 h-6" />,
title: 'Dijamin Aman',
description: 'Identitas pelapor dirahasiakan sepenuhnya sesuai kode etik.'
},
{
icon: '👥',
icon: <Users className="w-6 h-6" />,
title: 'Profesional',
description: 'Ditangani oleh tim yang berpengalaman'
description: 'Ditangani oleh tim ahli yang berpengalaman dan objektif.'
},
{
icon: '💚',
title: 'Support',
description: 'Dapatkan pendampingan penuh dari kami'
icon: <HeartHandshake className="w-6 h-6" />,
title: 'Pendampingan',
description: 'Dukungan psikologis dan hukum selama proses berjalan.'
}
];
return (
<section id="services" className="py-20 bg-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-20 left-20 w-72 h-72 bg-accent rounded-full blur-3xl"></div>
<div className="absolute bottom-20 right-20 w-96 h-96 bg-primary rounded-full blur-3xl"></div>
<section id="services" className="pt-20 pb-24 bg-gray-50 dark:bg-gray-900 relative overflow-hidden transition-colors duration-300">
{/* Background Decorations */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-purple-200/30 dark:bg-purple-900/10 rounded-full blur-3xl"></div>
<div className="absolute bottom-[-10%] left-[-5%] w-96 h-96 bg-blue-200/30 dark:bg-blue-900/10 rounded-full blur-3xl"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Cara <span className="text-primary">Melapor</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Pilih metode pelaporan yang paling nyaman untuk Anda. Kami siap membantu dengan profesionalisme dan kerahasiaan terjamin.
</motion.p>
</motion.div>
{/* Reporting Methods Cards */}
<motion.div
className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-20"
{/* Header Section */}
<div className="text-center mb-12">
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6 tracking-tight">
Cara <span className="text-[#191970] dark:text-blue-400">Melapor</span>
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto leading-relaxed">
Kami menyediakan ruang aman bagi Anda untuk bersuara. Pilih metode yang paling nyaman, kami siap mendampingi setiap langkahnya.
</p>
</div>
{/* Reporting Methods Grid */}
<motion.div
className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-24"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
@ -103,145 +123,175 @@ const Services = () => {
{reportingMethods.map((method, index) => (
<motion.div
key={method.id}
className="bg-white rounded-3xl p-8 shadow-soft hover:shadow-card transition-all duration-500 hover:-translate-y-2 group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 * index }}
whileHover={{ y: -5 }}
className="h-full"
>
{/* Icon */}
<motion.div
className="w-20 h-20 bg-gradient-to-br from-primary/10 to-accent/10 rounded-2xl flex items-center justify-center text-primary mb-6 group-hover:from-primary/20 group-hover:to-accent/20 transition-all duration-300"
whileHover={{ scale: 1.1, rotate: 5 }}
transition={{ duration: 0.3 }}
<SpotlightCard
className="h-full p-8 rounded-3xl border border-white/20 dark:border-white/10 bg-white/10 dark:bg-black/20 backdrop-blur-sm shadow-xl transition-all duration-300 group hover:bg-white/15 dark:hover:bg-white/5"
spotlightColor={method.spotlightColor}
>
{method.icon}
</motion.div>
{/* Gradient Blob Background */}
<div className={`absolute top-0 right-0 w-64 h-64 bg-gradient-to-br ${method.gradient} rounded-full blur-3xl -z-10 opacity-30 group-hover:opacity-60 transition-opacity duration-500`}></div>
{/* Content */}
<div className="space-y-4">
<h3 className="text-2xl font-bold text-gray-900 group-hover:text-primary transition-colors">
{method.title}
</h3>
<p className="text-gray-600 leading-relaxed">
{method.description}
</p>
{/* Features */}
<div className="space-y-3">
{method.features.map((feature, featureIndex) => (
<div key={featureIndex} className="flex items-center space-x-3">
<div className="w-2 h-2 bg-accent rounded-full flex-shrink-0"></div>
<span className="text-gray-700 text-sm font-medium">{feature}</span>
<div className="flex flex-col h-full justify-between relative z-10">
<div>
<div className={`w-16 h-16 rounded-2xl bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center mb-6 ${method.iconColor}`}>
{method.icon}
</div>
))}
</div>
{/* Button */}
<div className="pt-6">
{method.buttonLink.startsWith('http') ? (
<a
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
{method.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
{method.description}
</p>
<ul className="space-y-3 mb-8">
{method.features.map((feature, i) => (
<li key={i} className="flex items-center text-gray-700 dark:text-gray-300 text-sm font-medium">
<CheckCircle2 className={`w-4 h-4 mr-2 ${method.iconColor}`} />
{feature}
</li>
))}
</ul>
</div>
<div className="w-full flex justify-center mt-auto pt-6">
<FlowButton
text={method.buttonText}
href={method.buttonLink}
target="_blank"
rel="noopener noreferrer"
className={`inline-flex items-center justify-center px-8 py-4 ${method.buttonColor} text-white rounded-xl transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold w-full sm:w-auto shadow-soft`}
>
{method.buttonText}
</a>
) : (
<Link
to={method.buttonLink}
className={`inline-flex items-center justify-center px-8 py-4 ${method.buttonColor} text-white rounded-xl transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold w-full sm:w-auto shadow-soft`}
>
{method.buttonText}
</Link>
)}
target={method.buttonLink.startsWith('http') ? "_blank" : undefined}
colorStr={method.id === 'whatsapp' ? '#16a34a' : '#2563eb'}
hoverColorStr={method.id === 'whatsapp' ? '#16a34a' : '#1e40af'}
className="w-full max-w-[280px]"
/>
</div>
</div>
</div>
</SpotlightCard>
</motion.div>
))}
</motion.div>
{/* Important Information */}
<motion.div
className="bg-gradient-to-r from-purple-50 to-purple-100 rounded-3xl p-8 md:p-12"
{/* Support & Flow Section */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Trust Indicators (Left) */}
<motion.div
className="xl:col-span-1 bg-white dark:bg-gray-800 rounded-3xl p-8 border border-gray-100 dark:border-gray-700 shadow-md h-full"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Kenapa Kami?</h3>
<div className="space-y-6">
{importantInfo.map((info, index) => (
<div key={index} className="flex items-start space-x-4">
<div className="w-10 h-10 rounded-full bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center text-[#191970] dark:text-blue-300 flex-shrink-0">
{info.icon}
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white text-sm">{info.title}</h4>
<p className="text-gray-500 dark:text-gray-400 text-xs leading-relaxed mt-1">{info.description}</p>
</div>
</div>
))}
</div>
</motion.div>
{/* Process Flow (Right - Timeline) */}
<SpotlightCard
className="xl:col-span-2 relative overflow-hidden rounded-3xl p-8 md:p-10 border border-white/20 dark:border-white/10 bg-white/10 dark:bg-black/20 backdrop-blur-sm shadow-xl transition-all duration-300 group hover:bg-white/15 dark:hover:bg-white/5"
spotlightColor="rgba(25, 25, 112, 0.1)"
>
{/* Background Pattern */}
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-50 dark:bg-blue-900/10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-48 h-48 bg-purple-50 dark:bg-purple-900/10 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
<h3 className="text-2xl font-bold mb-12 relative z-10 text-gray-900 dark:text-white">Alur Penanganan</h3>
<div className="relative z-10 w-full">
{/* Progress Line Background */}
<div className="hidden md:block absolute top-[28px] left-0 w-full h-[2px] bg-gray-200/50 dark:bg-gray-700/50 rounded-full"></div>
{/* Animated Progress Line */}
<motion.div
className="hidden md:block absolute top-[28px] left-0 h-[2px] bg-[#191970] dark:bg-blue-500 origin-left z-0 rounded-full overflow-hidden"
initial={{ scaleX: 0 }}
whileInView={{ scaleX: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.5, ease: "easeInOut", delay: 0.2 }}
>
{/* Continuous Shimmer Animation */}
<motion.div
className="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white/50 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{
repeat: Infinity,
duration: 2,
ease: "linear",
repeatDelay: 0.5
}}
/>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8">
{handlingFlow.map((step, index) => (
<motion.div
key={index}
className="relative group/step pt-4 md:pt-0"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: index * 0.2 + 0.3 }}
>
<div className="relative z-10 flex flex-col items-center text-center">
<div className="w-14 h-14 rounded-full bg-white dark:bg-gray-800 border-2 border-gray-100 dark:border-gray-700 flex items-center justify-center mb-4 text-[#191970] dark:text-blue-500 shadow-md group-hover/step:scale-110 group-hover/step:border-[#191970] dark:group-hover/step:border-blue-500 transition-all duration-300 relative bg-opacity-80 backdrop-blur-md">
{step.icon}
</div>
<span className="text-xs font-bold font-mono text-[#191970] dark:text-blue-400 mb-2 opacity-80">{step.step}</span>
<h4 className="font-bold text-gray-900 dark:text-white text-base mb-2">{step.title}</h4>
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed font-medium">{step.desc}</p>
</div>
</motion.div>
))}
</div>
</div>
</SpotlightCard>
</div>
{/* Emergency Banner */}
<motion.div
className="mt-12 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/30 rounded-2xl p-6 flex flex-col md:flex-row items-center justify-between gap-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<div className="text-center mb-8">
<h3 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
Penting Untuk Diketahui
</h3>
<p className="text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Kami memastikan setiap proses pelaporan berjalan dengan aman, profesional, dan mendukung.
</p>
</div>
<motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-6"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{importantInfo.map((info, index) => (
<motion.div
key={index}
className="text-center p-6 bg-white rounded-2xl shadow-soft hover:shadow-card transition-all duration-300"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.05 }}
>
<div className="text-4xl mb-4">{info.icon}</div>
<h4 className="text-lg font-bold text-gray-900 mb-2">{info.title}</h4>
<p className="text-gray-600 text-sm leading-relaxed">{info.description}</p>
</motion.div>
))}
</motion.div>
</motion.div>
{/* Emergency Notice */}
<motion.div
className="mt-12 bg-gradient-to-r from-danger/10 to-danger/5 border border-danger/20 rounded-2xl p-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.6 }}
>
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-danger/20 rounded-xl flex items-center justify-center">
<svg className="w-6 h-6 text-danger" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center text-red-600 dark:text-red-400">
<Phone className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-gray-900 mb-1">Darurat? Hubungi Kami Sekarang</h4>
<p className="text-gray-600 text-sm">
Jika Anda atau orang lain berada dalam situasi darurat, segera hubungi WhatsApp kami untuk respons cepat 24/7.
</p>
</div>
<div className="flex-shrink-0">
<a
href="https://wa.me/6281234567890"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 bg-danger text-white rounded-xl hover:bg-danger-dark transition-all duration-300 font-semibold shadow-soft hover:shadow-lg"
>
Hubungi Darurat
</a>
<div>
<h4 className="font-bold text-gray-900 dark:text-white">Butuh Bantuan Darurat?</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">Jangan ragu untuk menghubungi kami jika situasi mendesak.</p>
</div>
</div>
<FlowButton
text="Hubungi Sekarang"
href="https://wa.me/6281234567890"
target="_blank"
colorStr="#dc2626"
hoverColorStr="#dc2626"
className="w-auto px-8"
/>
</motion.div>
</div>
</section>
);

View File

@ -0,0 +1,53 @@
import React from "react";
export const Component = ({
icon,
title,
subtitle,
size = "md",
className = "",
...props
}) => {
const sizes = {
sm: "px-10 py-4 rounded-full text-base",
md: "p-4 rounded-2xl",
lg: "p-6 rounded-3xl",
};
return (
<button
{...props}
className={`group relative overflow-hidden cursor-pointer transition-all duration-300 ease-out
shadow-lg hover:shadow-xl hover:scale-[1.02] hover:-translate-y-1 active:scale-95
border-0 flex items-center justify-between gap-6
${sizes[size]}
${className}`}>
{/* Subtle internal glow/shine for depth, but keeping base color solid */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-out"></div>
{/* Content - flat structure for perfect alignment */}
<div className="relative z-10 flex items-center gap-3">
{icon && React.cloneElement(icon, {
className: "w-6 h-6 text-white/90 group-hover:text-white transition-colors shrink-0",
})}
<span className="text-white font-bold text-base whitespace-nowrap">{title}</span>
</div>
{/* Arrow - Re-added as requested */}
<div className="relative z-10 opacity-70 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-300">
<svg
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
className="w-4 h-4 text-white">
<path
d="M9 5l7 7-7 7"
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"></path>
</svg>
</div>
</button>
);
};

View File

@ -0,0 +1,203 @@
"use client";;
import { ArrowLeft, ArrowRight } from "lucide-react";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel";
const data = [
{
id: "shadcn-ui",
title: "shadcn/ui: Building a Modern Component Library",
description:
"Explore how shadcn/ui revolutionized React component libraries by providing a unique approach to component distribution and customization, making it easier for developers to build beautiful, accessible applications.",
href: "https://ui.shadcn.com",
image:
"https://images.unsplash.com/photo-1551250928-243dc937c49d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjN8fHx8fHwyfHwxNzIzODA2OTM5fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "tailwind",
title: "Tailwind CSS: The Utility-First Revolution",
description:
"Discover how Tailwind CSS transformed the way developers style their applications, offering a utility-first approach that speeds up development while maintaining complete design flexibility.",
href: "https://tailwindcss.com",
image:
"https://images.unsplash.com/photo-1551250928-e4a05afaed1e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjR8fHx8fHwyfHwxNzIzODA2OTM5fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "astro",
title: "Astro: The All-in-One Web Framework",
description:
"Learn how Astro's innovative 'Islands Architecture' and zero-JS-by-default approach is helping developers build faster websites while maintaining rich interactivity where needed.",
href: "https://astro.build",
image:
"https://images.unsplash.com/photo-1536735561749-fc87494598cb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxNzd8fHx8fHwyfHwxNzIzNjM0NDc0fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "react",
title: "React: Pioneering Component-Based UI",
description:
"See how React continues to shape modern web development with its component-based architecture, enabling developers to build complex user interfaces with reusable, maintainable code.",
href: "https://react.dev",
image:
"https://images.unsplash.com/photo-1548324215-9133768e4094?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMzF8fHx8fHwyfHwxNzIzNDM1MzA1fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "nextjs",
title: "Next.js: The React Framework for Production",
description:
"Explore how Next.js has become the go-to framework for building full-stack React applications, offering features like server components, file-based routing, and automatic optimization.",
href: "https://nextjs.org",
image:
"https://images.unsplash.com/photo-1550070881-a5d71eda5800?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjV8fHx8fHwyfHwxNzIzNDM1Mjk4fA&ixlib=rb-4.0.3&q=80&w=1080",
},
];
const Gallery4 = ({
title = "Case Studies",
description = "Discover how leading companies and developers are leveraging modern web technologies to build exceptional digital experiences. These case studies showcase real-world applications and success stories.",
items = data
}) => {
const [carouselApi, setCarouselApi] = useState();
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (!carouselApi) {
return;
}
const updateSelection = () => {
setCanScrollPrev(carouselApi.canScrollPrev());
setCanScrollNext(carouselApi.canScrollNext());
setCurrentSlide(carouselApi.selectedScrollSnap());
};
updateSelection();
carouselApi.on("select", updateSelection);
return () => {
carouselApi.off("select", updateSelection);
};
}, [carouselApi]);
return (
<section className="py-32">
<div className="container mx-auto">
<div className="mb-8 flex items-end justify-between md:mb-14 lg:mb-16">
<div className="flex flex-col gap-4">
<h2 className="text-3xl font-medium md:text-4xl lg:text-5xl">
{title}
</h2>
<p className="max-w-lg text-muted-foreground">{description}</p>
</div>
<div className="hidden shrink-0 gap-2 md:flex">
<Button
size="icon"
variant="ghost"
onClick={() => {
carouselApi?.scrollPrev();
}}
disabled={!canScrollPrev}
className="disabled:pointer-events-auto">
<ArrowLeft className="size-5" />
</Button>
<Button
size="icon"
variant="ghost"
onClick={() => {
carouselApi?.scrollNext();
}}
disabled={!canScrollNext}
className="disabled:pointer-events-auto">
<ArrowRight className="size-5" />
</Button>
</div>
</div>
</div>
<div className="w-full">
<Carousel
setApi={setCarouselApi}
opts={{
align: "start",
loop: true,
breakpoints: {
"(max-width: 768px)": {
dragFree: true,
},
},
}}>
<CarouselContent
className="ml-0 2xl:ml-[max(8rem,calc(50vw-700px))] 2xl:mr-[max(0rem,calc(50vw-700px))]">
{items.map((item) => (
<CarouselItem key={item.id} className="max-w-[320px] pl-[20px] lg:max-w-[360px]">
{item.href.startsWith('/') ? (
<Link to={item.href} className="group rounded-xl">
<div
className="group relative h-full min-h-[27rem] max-w-full overflow-hidden rounded-xl md:aspect-[5/4] lg:aspect-[16/9]">
<img
src={item.image}
alt={item.title}
className="absolute h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105" />
<div
className="absolute inset-0 h-full bg-[linear-gradient(hsl(var(--primary)/0),hsl(var(--primary)/0.4),hsl(var(--primary)/0.8)_100%)] mix-blend-multiply" />
<div
className="absolute inset-x-0 bottom-0 flex flex-col items-start p-6 text-primary-foreground md:p-8">
<div className="mb-2 pt-4 text-xl font-semibold md:mb-3 md:pt-4 lg:pt-4">
{item.title}
</div>
<div className="mb-8 line-clamp-2 md:mb-12 lg:mb-9">
{item.description}
</div>
<div className="flex items-center text-sm">
Read more{" "}
<ArrowRight className="ml-2 size-5 transition-transform group-hover:translate-x-1" />
</div>
</div>
</div>
</Link>
) : (
<a href={item.href} target="_blank" rel="noopener noreferrer" className="group rounded-xl">
<div
className="group relative h-full min-h-[27rem] max-w-full overflow-hidden rounded-xl md:aspect-[5/4] lg:aspect-[16/9]">
<img
src={item.image}
alt={item.title}
className="absolute h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105" />
<div
className="absolute inset-0 h-full bg-[linear-gradient(hsl(var(--primary)/0),hsl(var(--primary)/0.4),hsl(var(--primary)/0.8)_100%)] mix-blend-multiply" />
<div
className="absolute inset-x-0 bottom-0 flex flex-col items-start p-6 text-primary-foreground md:p-8">
<div className="mb-2 pt-4 text-xl font-semibold md:mb-3 md:pt-4 lg:pt-4">
{item.title}
</div>
<div className="mb-8 line-clamp-2 md:mb-12 lg:mb-9">
{item.description}
</div>
<div className="flex items-center text-sm">
Read more{" "}
<ArrowRight className="ml-2 size-5 transition-transform group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
)}
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="mt-8 flex justify-center gap-2">
{items.map((_, index) => (
<button
key={index}
className={`h-2 w-2 rounded-full transition-colors ${currentSlide === index ? "bg-primary" : "bg-primary/20"
}`}
onClick={() => carouselApi?.scrollTo(index)}
aria-label={`Go to slide ${index + 1}`} />
))}
</div>
</div>
</section>
);
};
export { Gallery4 };

View File

@ -0,0 +1,251 @@
import React from 'react';
import styled from 'styled-components';
import { useTheme } from '../contexts/ThemeContext';
const Switch = () => {
const { toggleTheme, isDark } = useTheme();
const handleChange = () => {
toggleTheme();
};
return (
<StyledWrapper>
<label className="theme-switch">
<input
type="checkbox"
className="theme-switch__checkbox"
checked={isDark}
onChange={handleChange}
/>
<div className="theme-switch__container">
<div className="theme-switch__clouds" />
<div className="theme-switch__stars-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 55" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M135.831 3.00688C135.055 3.85027 134.111 4.29946 133 4.35447C134.111 4.40947 135.055 4.85867 135.831 5.71123C136.607 6.55462 136.996 7.56303 136.996 8.72727C136.996 7.95722 137.172 7.25134 137.525 6.59129C137.886 5.93124 138.372 5.39954 138.98 5.00535C139.598 4.60199 140.268 4.39114 141 4.35447C139.88 4.2903 138.936 3.85027 138.16 3.00688C137.384 2.16348 136.996 1.16425 136.996 0C136.996 1.16425 136.607 2.16348 135.831 3.00688ZM31 23.3545C32.1114 23.2995 33.0551 22.8503 33.8313 22.0069C34.6075 21.1635 34.9956 20.1642 34.9956 19C34.9956 20.1642 35.3837 21.1635 36.1599 22.0069C36.9361 22.8503 37.8798 23.2903 39 23.3545C38.2679 23.3911 37.5976 23.602 36.9802 24.0053C36.3716 24.3995 35.8864 24.9312 35.5248 25.5913C35.172 26.2513 34.9956 26.9572 34.9956 27.7273C34.9956 26.563 34.6075 25.5546 33.8313 24.7112C33.0551 23.8587 32.1114 23.4095 31 23.3545ZM0 36.3545C1.11136 36.2995 2.05513 35.8503 2.83131 35.0069C3.6075 34.1635 3.99559 33.1642 3.99559 32C3.99559 33.1642 4.38368 34.1635 5.15987 35.0069C5.93605 35.8503 6.87982 36.2903 8 36.3545C7.26792 36.3911 6.59757 36.602 5.98015 37.0053C5.37155 37.3995 4.88644 37.9312 4.52481 38.5913C4.172 39.2513 3.99559 39.9572 3.99559 40.7273C3.99559 39.563 3.6075 38.5546 2.83131 37.7112C2.05513 36.8587 1.11136 36.4095 0 36.3545ZM56.8313 24.0069C56.0551 24.8503 55.1114 25.2995 54 25.3545C55.1114 25.4095 56.0551 25.8587 56.8313 26.7112C57.6075 27.5546 57.9956 28.563 57.9956 29.7273C57.9956 28.9572 58.172 28.2513 58.5248 27.5913C58.8864 26.9312 59.3716 26.3995 59.9802 26.0053C60.5976 25.602 61.2679 25.3911 62 25.3545C60.8798 25.2903 59.9361 24.8503 59.1599 24.0069C58.3837 23.1635 57.9956 22.1642 57.9956 21C57.9956 22.1642 57.6075 23.1635 56.8313 24.0069ZM81 25.3545C82.1114 25.2995 83.0551 24.8503 83.8313 24.0069C84.6075 23.1635 84.9956 22.1642 84.9956 21C84.9956 22.1642 85.3837 23.1635 86.1599 24.0069C86.9361 24.8503 87.8798 25.2903 89 25.3545C88.2679 25.3911 87.5976 25.602 86.9802 26.0053C86.3716 26.3995 85.8864 26.9312 85.5248 27.5913C85.172 28.2513 84.9956 28.9572 84.9956 29.7273C84.9956 28.563 84.6075 27.5546 83.8313 26.7112C83.0551 25.8587 82.1114 25.4095 81 25.3545ZM136 36.3545C137.111 36.2995 138.055 35.8503 138.831 35.0069C139.607 34.1635 139.996 33.1642 139.996 32C139.996 33.1642 140.384 34.1635 141.16 35.0069C141.936 35.8503 142.88 36.2903 144 36.3545C143.268 36.3911 142.598 36.602 141.98 37.0053C141.372 37.3995 140.886 37.9312 140.525 38.5913C140.172 39.2513 139.996 39.9572 139.996 40.7273C139.996 39.563 139.607 38.5546 138.831 37.7112C138.055 36.8587 137.111 36.4095 136 36.3545ZM101.831 49.0069C101.055 49.8503 100.111 50.2995 99 50.3545C100.111 50.4095 101.055 50.8587 101.831 51.7112C102.607 52.5546 102.996 53.563 102.996 54.7273C102.996 53.9572 103.172 53.2513 103.525 52.5913C103.886 51.9312 104.372 51.3995 104.98 51.0053C105.598 50.602 106.268 50.3911 107 50.3545C105.88 50.2903 104.936 49.8503 104.16 49.0069C103.384 48.1635 102.996 47.1642 102.996 46C102.996 47.1642 102.607 48.1635 101.831 49.0069Z"
fill="currentColor" />
</svg>
</div>
<div className="theme-switch__circle-container">
<div className="theme-switch__sun-moon-container">
<div className="theme-switch__moon">
<div className="theme-switch__spot" />
<div className="theme-switch__spot" />
<div className="theme-switch__spot" />
</div>
</div>
</div>
</div>
</label>
</StyledWrapper>
);
}
const StyledWrapper = styled.div`
.theme-switch {
--toggle-size: 7px;
/* the size is adjusted using font-size,
this is not transform scale,
so you can choose any size */
--container-width: 5.625em;
--container-height: 2.5em;
--container-radius: 6.25em;
/* radius 0 - minecraft mode :) */
--container-light-bg: #3D7EAE;
--container-night-bg: #1D1F2C;
--circle-container-diameter: 3.375em;
--sun-moon-diameter: 2.125em;
--sun-bg: #ECCA2F;
--moon-bg: #C4C9D1;
--spot-color: #959DB1;
--circle-container-offset: calc((var(--circle-container-diameter) - var(--container-height)) / 2 * -1);
--stars-color: #fff;
--clouds-color: #F3FDFF;
--back-clouds-color: #AACADF;
--transition: .5s cubic-bezier(0, -0.02, 0.4, 1.25);
--circle-transition: .3s cubic-bezier(0, -0.02, 0.35, 1.17);
}
.theme-switch, .theme-switch *, .theme-switch *::before, .theme-switch *::after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
font-size: var(--toggle-size);
}
.theme-switch__container {
width: var(--container-width);
height: var(--container-height);
background-color: var(--container-light-bg);
border-radius: var(--container-radius);
overflow: hidden;
cursor: pointer;
-webkit-box-shadow: 0em -0.062em 0.062em rgba(0, 0, 0, 0.25), 0em 0.062em 0.125em rgba(255, 255, 255, 0.94);
box-shadow: 0em -0.062em 0.062em rgba(0, 0, 0, 0.25), 0em 0.062em 0.125em rgba(255, 255, 255, 0.94);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
position: relative;
}
.theme-switch__container::before {
content: "";
position: absolute;
z-index: 1;
inset: 0;
-webkit-box-shadow: 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset, 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset;
box-shadow: 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset, 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset;
border-radius: var(--container-radius)
}
.theme-switch__checkbox {
display: none;
}
.theme-switch__circle-container {
width: var(--circle-container-diameter);
height: var(--circle-container-diameter);
background-color: rgba(255, 255, 255, 0.1);
position: absolute;
left: var(--circle-container-offset);
top: var(--circle-container-offset);
border-radius: var(--container-radius);
-webkit-box-shadow: inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), 0 0 0 0.625em rgba(255, 255, 255, 0.1), 0 0 0 1.25em rgba(255, 255, 255, 0.1);
box-shadow: inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), 0 0 0 0.625em rgba(255, 255, 255, 0.1), 0 0 0 1.25em rgba(255, 255, 255, 0.1);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-transition: var(--circle-transition);
-o-transition: var(--circle-transition);
transition: var(--circle-transition);
pointer-events: none;
}
.theme-switch__sun-moon-container {
pointer-events: auto;
position: relative;
z-index: 2;
width: var(--sun-moon-diameter);
height: var(--sun-moon-diameter);
margin: auto;
border-radius: var(--container-radius);
background-color: var(--sun-bg);
-webkit-box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #a1872a inset;
box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #a1872a inset;
-webkit-filter: drop-shadow(0.062em 0.125em 0.125em rgba(0, 0, 0, 0.25)) drop-shadow(0em 0.062em 0.125em rgba(0, 0, 0, 0.25));
filter: drop-shadow(0.062em 0.125em 0.125em rgba(0, 0, 0, 0.25)) drop-shadow(0em 0.062em 0.125em rgba(0, 0, 0, 0.25));
overflow: hidden;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.theme-switch__moon {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
width: 100%;
height: 100%;
background-color: var(--moon-bg);
border-radius: inherit;
-webkit-box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #969696 inset;
box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #969696 inset;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
position: relative;
}
.theme-switch__spot {
position: absolute;
top: 0.75em;
left: 0.312em;
width: 0.75em;
height: 0.75em;
border-radius: var(--container-radius);
background-color: var(--spot-color);
-webkit-box-shadow: 0em 0.0312em 0.062em rgba(0, 0, 0, 0.25) inset;
box-shadow: 0em 0.0312em 0.062em rgba(0, 0, 0, 0.25) inset;
}
.theme-switch__spot:nth-of-type(2) {
width: 0.375em;
height: 0.375em;
top: 0.937em;
left: 1.375em;
}
.theme-switch__spot:nth-last-of-type(3) {
width: 0.25em;
height: 0.25em;
top: 0.312em;
left: 0.812em;
}
.theme-switch__clouds {
width: 1.25em;
height: 1.25em;
background-color: var(--clouds-color);
border-radius: var(--container-radius);
position: absolute;
bottom: -0.625em;
left: 0.312em;
-webkit-box-shadow: 0.937em 0.312em var(--clouds-color), -0.312em -0.312em var(--back-clouds-color), 1.437em 0.375em var(--clouds-color), 0.5em -0.125em var(--back-clouds-color), 2.187em 0 var(--clouds-color), 1.25em -0.062em var(--back-clouds-color), 2.937em 0.312em var(--clouds-color), 2em -0.312em var(--back-clouds-color), 3.625em -0.062em var(--clouds-color), 2.625em 0em var(--back-clouds-color), 4.5em -0.312em var(--clouds-color), 3.375em -0.437em var(--back-clouds-color), 4.625em -1.75em 0 0.437em var(--clouds-color), 4em -0.625em var(--back-clouds-color), 4.125em -2.125em 0 0.437em var(--back-clouds-color);
box-shadow: 0.937em 0.312em var(--clouds-color), -0.312em -0.312em var(--back-clouds-color), 1.437em 0.375em var(--clouds-color), 0.5em -0.125em var(--back-clouds-color), 2.187em 0 var(--clouds-color), 1.25em -0.062em var(--back-clouds-color), 2.937em 0.312em var(--clouds-color), 2em -0.312em var(--back-clouds-color), 3.625em -0.062em var(--clouds-color), 2.625em 0em var(--back-clouds-color), 4.5em -0.312em var(--clouds-color), 3.375em -0.437em var(--back-clouds-color), 4.625em -1.75em 0 0.437em var(--clouds-color), 4em -0.625em var(--back-clouds-color), 4.125em -2.125em 0 0.437em var(--back-clouds-color);
-webkit-transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
-o-transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
}
.theme-switch__stars-container {
position: absolute;
color: var(--stars-color);
top: -100%;
left: 0.312em;
width: 2.75em;
height: auto;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
/* actions */
.theme-switch__checkbox:checked + .theme-switch__container {
background-color: var(--container-night-bg);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__circle-container {
left: calc(100% - var(--circle-container-offset) - var(--circle-container-diameter));
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__circle-container:hover {
left: calc(100% - var(--circle-container-offset) - var(--circle-container-diameter) - 0.187em)
}
.theme-switch__circle-container:hover {
left: calc(var(--circle-container-offset) + 0.187em);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__moon {
-webkit-transform: translate(0);
-ms-transform: translate(0);
transform: translate(0);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__clouds {
bottom: -4.062em;
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__stars-container {
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}`;
export default Switch;

View File

@ -0,0 +1,369 @@
"use client";
import { TimelineContent } from "@/components/ui/timeline-animation";
import { VerticalCutReveal } from "@/components/ui/vertical-cut-reveal";
import { ArrowRight, Shield, Heart, Lock, Scale, Phone } from "lucide-react";
import { useRef, useEffect, useState } from "react";
import { useInView, motion, AnimatePresence } from "framer-motion";
export default function AboutSection() {
const heroRef = useRef(null);
const counterRef = useRef(null);
const isCounterInView = useInView(counterRef, { once: true });
const [count, setCount] = useState(0);
// Carousel state
const heroImages = ["/Gambar1.jpg", "/Gambar2.jpeg", "/Gambar3.jpeg"];
const [slideIndex, setSlideIndex] = useState(0);
const [progress, setProgress] = useState(0);
const SLIDE_DURATION = 5000; // 5 seconds
useEffect(() => {
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) return 0;
return prev + (100 / (SLIDE_DURATION / 50));
});
}, 50);
const slideTimer = setInterval(() => {
setSlideIndex((prev) => (prev + 1) % heroImages.length);
setProgress(0);
}, SLIDE_DURATION);
return () => {
clearInterval(progressInterval);
clearInterval(slideTimer);
};
}, []);
useEffect(() => {
if (!isCounterInView) return;
let start = 0;
const end = 100;
const duration = 2000;
const stepTime = duration / end;
const timer = setInterval(() => {
start += 1;
setCount(start);
if (start >= end) clearInterval(timer);
}, stepTime);
return () => clearInterval(timer);
}, [isCounterInView]);
const revealVariants = {
visible: (i) => ({
y: 0,
opacity: 1,
filter: "blur(0px)",
transition: {
delay: i * 0.15,
duration: 0.3,
},
}),
hidden: {
filter: "blur(10px)",
y: -20,
opacity: 0,
},
};
const scaleVariants = {
visible: (i) => ({
opacity: 1,
filter: "blur(0px)",
transition: {
delay: i * 0.15,
duration: 0.3,
},
}),
hidden: {
filter: "blur(10px)",
opacity: 0,
},
};
return (
<section className="min-h-screen py-16 bg-[#f9f9f9] flex items-center" ref={heroRef} id="about">
<div className="max-w-[90rem] mx-auto w-full px-8 lg:px-12">
<div className="relative">
{/* Header */}
<div className="flex justify-between items-center mb-8 w-[85%] absolute lg:top-4 md:top-0 sm:-top-2 -top-3 z-10">
<div className="flex items-center gap-2 text-xl">
<span className="text-[#191970] animate-spin"></span>
<TimelineContent
as="span"
animationNum={0}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-sm font-medium text-gray-600"
>
TENTANG KAMI
</TimelineContent>
</div>
<div className="flex gap-3">
<TimelineContent
as="div"
animationNum={0}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Shield className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={1}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Heart className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={2}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Lock className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={3}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Scale className="w-4 h-4 text-[#191970]" />
</TimelineContent>
</div>
</div>
{/* Hero Image with clip path */}
<TimelineContent
as="figure"
animationNum={4}
timelineRef={heroRef}
customVariants={scaleVariants}
className="relative group"
>
<svg
className="w-full"
width={"100%"}
height={"100%"}
viewBox="0 0 100 40"
>
<defs>
<clipPath
id="clip-inverted"
clipPathUnits={"userSpaceOnUse"}
>
<path
transform="scale(100, 40)"
d="M0.0998072 1H0.422076H0.749756C0.767072 1 0.774207 0.961783 0.77561 0.942675V0.807325C0.777053 0.743631 0.791844 0.731953 0.799059 0.734076H0.969813C0.996268 0.730255 1.00088 0.693206 0.999875 0.675159V0.0700637C0.999875 0.0254777 0.985045 0.00477707 0.977629 0H0.902473C0.854975 0 0.890448 0.138535 0.850165 0.138535H0.0204424C0.00408849 0.142357 0 0.180467 0 0.199045V0.410828C0 0.449045 0.0136283 0.46603 0.0204424 0.469745H0.0523086C0.0696245 0.471019 0.0735527 0.497877 0.0733523 0.511146V0.915605C0.0723903 0.983121 0.090588 1 0.0998072 1Z"
fill="#D9D9D9"
/>
</clipPath>
</defs>
<g clipPath="url(#clip-inverted)">
<AnimatePresence>
<motion.image
key={slideIndex}
preserveAspectRatio="xMidYMid slice"
width={"100%"}
height={"100%"}
xlinkHref={heroImages[slideIndex]}
initial={{ x: 100 }}
animate={{ x: 0 }}
exit={{ x: -100 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
/>
</AnimatePresence>
</g>
</svg>
{/* Slide progress indicators */}
<div className="absolute bottom-[3%] left-[10%] flex gap-3 items-center z-10">
{heroImages.map((_, i) => (
<div
key={i}
className="h-[6px] rounded-full overflow-hidden cursor-pointer shadow-sm transition-all duration-300"
style={{
width: i === slideIndex ? '3rem' : '1.5rem',
backgroundColor: 'rgba(255,255,255,0.4)'
}}
onClick={() => { setSlideIndex(i); setProgress(0); }}
>
<div
className="h-full rounded-full transition-all duration-100 ease-linear"
style={{
width: i === slideIndex ? `${progress}%` : i < slideIndex ? '100%' : '0%',
backgroundColor: '#fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
}}
/>
</div>
))}
</div>
{/* 100% stat in the white cutout */}
<TimelineContent
as="div"
animationNum={5}
timelineRef={heroRef}
customVariants={revealVariants}
className="absolute bottom-[10%] right-[0.5%] text-right"
>
<div className="flex items-baseline gap-2 justify-end" ref={counterRef}>
<span className="text-[#191970] font-extrabold text-2xl sm:text-3xl lg:text-4xl leading-none" style={{ fontFamily: "'Inter', sans-serif" }}>{count}%</span>
<span className="text-gray-500 text-2xl sm:text-3xl lg:text-4xl uppercase tracking-wider font-normal leading-none">Rahasia</span>
</div>
<span className="text-gray-400 text-xs sm:text-sm mt-0.5 block text-left">kerahasiaan terjamin</span>
</TimelineContent>
</TimelineContent>
{/* Stats */}
<div className="flex flex-wrap items-center justify-start gap-6 py-4 text-sm">
<TimelineContent
as="div"
animationNum={5}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">24/7</span>
<span className="text-gray-600">layanan aktif</span>
</TimelineContent>
<span className="text-gray-300">|</span>
<TimelineContent
as="div"
animationNum={7}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">Gratis</span>
<span className="text-gray-600">tanpa biaya</span>
</TimelineContent>
<span className="text-gray-300">|</span>
<TimelineContent
as="div"
animationNum={8}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">Cepat</span>
<span className="text-gray-600">respon tanggap</span>
</TimelineContent>
</div>
</div>
{/* Main Content */}
<div className="grid md:grid-cols-3 gap-8">
<div className="md:col-span-2">
<h2 className="sm:text-4xl md:text-5xl text-2xl !leading-[110%] font-semibold text-gray-900 mb-8">
<VerticalCutReveal
splitBy="words"
staggerDuration={0.1}
staggerFrom="first"
reverse={true}
transition={{
type: "spring",
stiffness: 250,
damping: 30,
delay: 1,
}}
>
Menciptakan Kampus Aman & Bermartabat.
</VerticalCutReveal>
</h2>
<TimelineContent
as="div"
animationNum={9}
timelineRef={heroRef}
customVariants={revealVariants}
className="grid md:grid-cols-2 gap-8 text-gray-600"
>
<TimelineContent
as="div"
animationNum={10}
timelineRef={heroRef}
customVariants={revealVariants}
className="sm:text-base text-xs"
>
<p className="leading-relaxed text-justify">
PolijeCare merupakan kanal resmi pengaduan Satgas Pencegahan
dan Penanganan Kekerasan Seksual (PPKPT) Politeknik Negeri
Jember. Kami hadir sebagai garda terdepan dalam melindungi
korban dan memberikan pendampingan profesional.
</p>
</TimelineContent>
<TimelineContent
as="div"
animationNum={11}
timelineRef={heroRef}
customVariants={revealVariants}
className="sm:text-base text-xs"
>
<p className="leading-relaxed text-justify">
Setiap laporan ditangani secara empatik, profesional, dan
menjaga kerahasiaan penuh. Kami percaya bahwa setiap individu
berhak mendapat rasa aman dalam menempuh pendidikan tanpa
ancaman kekerasan seksual.
</p>
</TimelineContent>
</TimelineContent>
</div>
<div className="md:col-span-1">
<div className="text-right">
<TimelineContent
as="div"
animationNum={12}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-[#191970] text-2xl font-bold mb-2"
>
SATGAS PPKPT
</TimelineContent>
<TimelineContent
as="div"
animationNum={13}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-gray-600 text-sm mb-8"
>
Politeknik Negeri Jember
</TimelineContent>
<TimelineContent
as="div"
animationNum={14}
timelineRef={heroRef}
customVariants={revealVariants}
className="mb-6"
>
<p className="text-gray-900 font-medium mb-4">
Siap untuk melaporkan atau butuh bantuan? Kami siap mendengarkan Anda.
</p>
</TimelineContent>
<TimelineContent
as="a"
animationNum={15}
timelineRef={heroRef}
customVariants={revealVariants}
href="#contact"
className="group relative inline-flex w-fit ml-auto items-center gap-2 px-7 py-3 bg-[#191970] text-white rounded-full font-semibold text-sm cursor-pointer transition-all duration-300 hover:bg-[#1a237e] hover:text-white hover:shadow-[0_4px_20px_rgba(26,35,126,0.5)] hover:gap-3"
>
<Phone className="w-4 h-4 opacity-60" />
HUBUNGI KAMI
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</TimelineContent>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,47 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,193 @@
import * as React from "react"
import useEmblaCarousel from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
const CarouselContext = React.createContext(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef((
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref,
) => {
const [carouselRef, api] = useEmblaCarousel({
...opts,
axis: orientation === "horizontal" ? "x" : "y",
}, plugins)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback((event) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}, [scrollPrev, scrollNext])
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
};
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}>
{children}
</div>
</CarouselContext.Provider>
);
})
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props} />
</div>
);
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props} />
);
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
})
CarouselNext.displayName = "CarouselNext"
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,120 @@
"use client";;
import * as React from "react";
import { AnimatePresence, motion } from "framer-motion";
import { useOnClickOutside } from "usehooks-ts";
import { cn } from "@/lib/utils";
const buttonVariants = {
initial: {
gap: 0,
paddingLeft: ".5rem",
paddingRight: ".5rem",
},
animate: (isExpanded) => ({
gap: isExpanded ? ".5rem" : 0,
paddingLeft: isExpanded ? "2rem" : "1rem",
paddingRight: isExpanded ? "2rem" : "1rem",
}),
};
const spanVariants = {
initial: { width: 0, opacity: 0 },
animate: { width: "auto", opacity: 1 },
exit: { width: 0, opacity: 0 },
};
const transition = { type: "spring", bounce: 0, duration: 0.3 };
export function ExpandableTabs({
tabs,
className,
activeColor = "text-primary",
onChange,
activeTab,
trailingElement
}) {
const [selected, setSelected] = React.useState(null);
const [hovered, setHovered] = React.useState(null);
const outsideClickRef = React.useRef(null);
React.useEffect(() => {
if (activeTab !== undefined && activeTab !== null) {
setSelected(activeTab);
}
}, [activeTab]);
useOnClickOutside(outsideClickRef, () => {
// Keep the active tab visible don't clear selection on outside click
});
const handleSelect = (index) => {
setSelected(index);
onChange?.(index);
};
const Separator = () => (
<div className="mx-1 h-[24px] w-[1.2px] bg-border" aria-hidden="true" />
);
return (
<div
ref={outsideClickRef}
className={cn(
"flex flex-wrap items-center gap-2 rounded-full border border-white/20 bg-white/80 backdrop-blur-md p-1 shadow-lg",
className
)}>
{tabs.map((tab, index) => {
if (tab.type === "separator") {
return <Separator key={`separator-${index}`} />;
}
const Icon = tab.icon;
const isExpanded = selected === index || hovered === index;
return (
<motion.button
key={tab.title}
layout
variants={buttonVariants}
initial={false}
animate="animate"
custom={isExpanded}
onClick={() => handleSelect(index)}
onMouseEnter={() => setHovered(index)}
onMouseLeave={() => setHovered(null)}
transition={transition}
className={cn(
"relative flex items-center rounded-full py-3 text-base font-medium transition-colors duration-300",
selected === index
? "bg-[#191970] text-white shadow-[0_4px_15px_rgba(25,25,112,0.4)]"
: hovered === index
? "bg-gray-200 text-foreground"
: "text-muted-foreground hover:text-foreground"
)}>
<Icon size={24} />
<AnimatePresence initial={false}>
{isExpanded && (
<motion.span
variants={spanVariants}
initial="initial"
animate="animate"
exit="exit"
transition={transition}
className="overflow-hidden whitespace-nowrap">
{tab.title}
</motion.span>
)}
</AnimatePresence>
</motion.button>
);
})}
{trailingElement && (
<>
<Separator />
<div className="pl-1">
{trailingElement}
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,53 @@
// This is file of your component
// You can use any dependencies from npm; we import them automatically in package.json
'use client';
import { ArrowRight } from 'lucide-react';
export function FlowButton({
text = "Button",
href,
target,
rel,
className = "",
colorStr = "#111111", // Default text/border color
hoverColorStr = "#111111", // Circle bg color on hover
}) {
const Component = href ? 'a' : 'button';
return (
<Component
href={href}
target={target}
rel={rel}
className={`group relative flex items-center justify-center gap-1 overflow-hidden rounded-[100px] border-[1.5px] bg-transparent px-8 py-3 text-sm font-semibold cursor-pointer transition-all duration-[600ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-transparent hover:!text-white hover:rounded-[12px] active:scale-[0.95] ${className}`}
style={{
borderColor: `${colorStr}40`, // 40 is hex opacity
color: colorStr
}}
>
{/* Left arrow */}
<ArrowRight
className="absolute w-4 h-4 left-[-25%] fill-none z-[9] group-hover:left-4 group-hover:!stroke-white transition-all duration-[800ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
style={{ stroke: colorStr }}
/>
{/* Text */}
<span className="relative z-[1] -translate-x-3 group-hover:translate-x-3 transition-all duration-[800ms] ease-out group-hover:!text-white">
{text}
</span>
{/* Circle Hover Effect */}
<span
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 rounded-[50%] opacity-0 group-hover:w-[1200px] group-hover:h-[1200px] group-hover:opacity-100 transition-all duration-[800ms] ease-[cubic-bezier(0.19,1,0.22,1)]"
style={{ backgroundColor: hoverColorStr }}
></span>
{/* Right arrow */}
<ArrowRight
className="absolute w-4 h-4 right-4 fill-none z-[9] group-hover:right-[-25%] group-hover:!stroke-white transition-all duration-[800ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
style={{ stroke: colorStr }}
/>
</Component>
);
}

View File

@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />
);
})
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,92 @@
import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence, useScroll, useMotionValueEvent } from "framer-motion";
import { Plus, Heart, Star } from "lucide-react";
export function ScrollParticles() {
const { scrollY } = useScroll();
const [particles, setParticles] = useState([]);
const [windowHeight, setWindowHeight] = useState(0);
const lastY = useRef(0);
useEffect(() => {
if (typeof window !== "undefined") {
setWindowHeight(window.innerHeight);
const handleResize = () => setWindowHeight(window.innerHeight);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}
}, []);
useMotionValueEvent(scrollY, "change", (latest) => {
const diff = Math.abs(latest - lastY.current);
const direction = latest > lastY.current ? 1 : -1;
// Spawn particles if scrolling fast enough
if (diff > 5 && windowHeight > 0) {
const scrollHeight = document.documentElement.scrollHeight - windowHeight;
const progress = latest / scrollHeight;
// Calculate approximate scrollbar thumb position (center of thumb)
const thumbY = progress * (windowHeight - 40);
const shapes = ["plus", "heart", "star"];
const randomShape = shapes[Math.floor(Math.random() * shapes.length)];
const newParticle = {
id: Date.now() + Math.random(),
top: thumbY + (Math.random() * 40 - 20), // Wider spread
left: -Math.random() * 20 - 10,
size: Math.random() * 14 + 10, // Larger size: 10px - 24px
color: Math.random() > 0.5 ? "#191970" : "#4C6EF5",
shape: randomShape,
rotation: Math.random() * 360,
velocity: {
x: -Math.random() * 30 - 20, // Faster drift left
y: (Math.random() * 30 - 15) + (direction * 8)
}
};
setParticles(prev => [...prev.slice(-20), newParticle]);
}
lastY.current = latest;
});
return (
<div className="fixed top-0 right-0 w-4 h-full pointer-events-none z-[9999]">
<AnimatePresence>
{particles.map((particle) => (
<motion.div
key={particle.id}
initial={{
opacity: 1,
x: 0,
y: particle.top,
scale: 0,
rotate: particle.rotation
}}
animate={{
opacity: 0,
x: particle.velocity.x,
y: particle.top + particle.velocity.y,
scale: 1,
rotate: particle.rotation + 180
}}
exit={{ opacity: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="absolute right-1 flex items-center justify-center"
style={{
width: particle.size,
height: particle.size,
color: particle.color,
}}
>
{particle.shape === "plus" && <Plus size={particle.size} strokeWidth={3} />}
{particle.shape === "heart" && <Heart size={particle.size} fill={particle.color} strokeWidth={0} />}
{particle.shape === "star" && <Star size={particle.size} fill={particle.color} strokeWidth={0} />}
</motion.div>
))}
</AnimatePresence>
</div>
);
}

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,43 @@
import React, { useRef, useState } from "react";
export const SpotlightCard = ({ children, className = "", spotlightColor = "rgba(255, 255, 255, 0.25)" }) => {
const divRef = useRef(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0);
const handleMouseMove = (e) => {
if (!divRef.current) return;
const rect = divRef.current.getBoundingClientRect();
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};
const handleMouseEnter = () => {
setOpacity(1);
};
const handleMouseLeave = () => {
setOpacity(0);
};
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`relative overflow-hidden rounded-3xl border border-gray-100 dark:border-gray-800 shadow-lg ${className}`}
>
<div
className="pointer-events-none absolute -inset-px transition duration-300"
style={{
opacity,
background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, ${spotlightColor}, transparent 40%)`,
}}
/>
<div className="relative h-full">
{children}
</div>
</div>
);
};

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)} />
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,65 @@
"use client";
import React, { useRef, createElement, forwardRef } from "react";
import {
motion,
useInView,
useScroll,
useTransform,
} from "framer-motion";
import { cn } from "@/lib/utils";
const TimelineContent = forwardRef(
(
{
as = "div",
children,
className,
animationNum = 0,
timelineRef,
customVariants,
...props
},
ref
) => {
const defaultVariants = {
hidden: {
opacity: 0,
y: 20,
filter: "blur(8px)",
},
visible: (i) => ({
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: {
delay: i * 0.3,
duration: 0.5,
ease: "easeOut",
},
}),
};
const variants = customVariants || defaultVariants;
const isInView = useInView(timelineRef, { once: true, amount: 0.1 });
const MotionComponent = motion[as] || motion.div;
return (
<MotionComponent
ref={ref}
className={cn(className)}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
variants={variants}
custom={animationNum}
{...props}
>
{children}
</MotionComponent>
);
}
);
TimelineContent.displayName = "TimelineContent";
export { TimelineContent };

View File

@ -0,0 +1,189 @@
"use client";
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
const VerticalCutReveal = forwardRef(
(
{
children,
reverse = false,
transition = {
type: "spring",
stiffness: 190,
damping: 22,
},
splitBy = "words",
staggerDuration = 0.2,
staggerFrom = "first",
containerClassName,
wordLevelClassName,
elementLevelClassName,
onClick,
onStart,
onComplete,
autoStart = true,
...props
},
ref
) => {
const containerRef = useRef(null);
const text =
typeof children === "string" ? children : children?.toString() || "";
const [isAnimating, setIsAnimating] = useState(false);
const splitIntoCharacters = (text) => {
if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return Array.from(segmenter.segment(text), ({ segment }) => segment);
}
return Array.from(text);
};
const elements = useMemo(() => {
const words = text.split(" ");
if (splitBy === "characters") {
return words.map((word, i) => ({
characters: splitIntoCharacters(word),
needsSpace: i !== words.length - 1,
}));
}
return splitBy === "words"
? text.split(" ")
: splitBy === "lines"
? text.split("\n")
: text.split(splitBy);
}, [text, splitBy]);
const getStaggerDelay = useCallback(
(index) => {
const total =
splitBy === "characters"
? elements.reduce(
(acc, word) =>
acc +
(typeof word === "string"
? 1
: word.characters.length + (word.needsSpace ? 1 : 0)),
0
)
: elements.length;
if (staggerFrom === "first") return index * staggerDuration;
if (staggerFrom === "last")
return (total - 1 - index) * staggerDuration;
if (staggerFrom === "center") {
const center = Math.floor(total / 2);
return Math.abs(center - index) * staggerDuration;
}
if (staggerFrom === "random") {
const randomIndex = Math.floor(Math.random() * total);
return Math.abs(randomIndex - index) * staggerDuration;
}
return Math.abs(staggerFrom - index) * staggerDuration;
},
[elements.length, staggerFrom, staggerDuration]
);
const startAnimation = useCallback(() => {
setIsAnimating(true);
onStart?.();
}, [onStart]);
useImperativeHandle(ref, () => ({
startAnimation,
reset: () => setIsAnimating(false),
}));
useEffect(() => {
if (autoStart) {
startAnimation();
}
}, [autoStart]);
const variants = {
hidden: { y: reverse ? "-100%" : "100%" },
visible: (i) => ({
y: 0,
transition: {
...transition,
delay: (transition?.delay || 0) + getStaggerDelay(i),
},
}),
};
return (
<span
className={cn(
containerClassName,
"flex flex-wrap whitespace-pre-wrap",
splitBy === "lines" && "flex-col"
)}
onClick={onClick}
ref={containerRef}
{...props}
>
<span className="sr-only">{text}</span>
{(splitBy === "characters"
? elements
: elements.map((el, i) => ({
characters: [el],
needsSpace: i !== elements.length - 1,
}))
).map((wordObj, wordIndex, array) => {
const previousCharsCount = array
.slice(0, wordIndex)
.reduce((sum, word) => sum + word.characters.length, 0);
return (
<span
key={wordIndex}
aria-hidden="true"
className={cn("inline-flex overflow-hidden", wordLevelClassName)}
>
{wordObj.characters.map((char, charIndex) => (
<span
className={cn(
elementLevelClassName,
"whitespace-pre-wrap relative"
)}
key={charIndex}
>
<motion.span
custom={previousCharsCount + charIndex}
initial="hidden"
animate={isAnimating ? "visible" : "hidden"}
variants={variants}
onAnimationComplete={
wordIndex === elements.length - 1 &&
charIndex === wordObj.characters.length - 1
? onComplete
: undefined
}
className="inline-block"
>
{char}
</motion.span>
</span>
))}
{wordObj.needsSpace && <span> </span>}
</span>
);
})}
</span>
);
}
);
VerticalCutReveal.displayName = "VerticalCutReveal";
export { VerticalCutReveal };

View File

@ -0,0 +1,264 @@
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import axios from '../utils/axiosConfig';
// Initial state
const initialState = {
user: null,
token: localStorage.getItem('token'),
isAuthenticated: false,
isLoading: false, // Changed to false initially
error: null,
};
// Action types
const AUTH_ACTIONS = {
LOGIN_START: 'LOGIN_START',
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
LOGIN_FAILURE: 'LOGIN_FAILURE',
LOGOUT: 'LOGOUT',
LOAD_USER_START: 'LOAD_USER_START',
LOAD_USER_SUCCESS: 'LOAD_USER_SUCCESS',
LOAD_USER_FAILURE: 'LOAD_USER_FAILURE',
CLEAR_ERROR: 'CLEAR_ERROR',
};
// Reducer
const authReducer = (state, action) => {
switch (action.type) {
case AUTH_ACTIONS.LOGIN_START:
return {
...state,
isLoading: true,
error: null,
};
case AUTH_ACTIONS.LOGIN_SUCCESS:
return {
...state,
isLoading: false,
isAuthenticated: true,
user: action.payload.user,
token: action.payload.token,
error: null,
};
case AUTH_ACTIONS.LOGIN_FAILURE:
return {
...state,
isLoading: false,
isAuthenticated: false,
user: null,
token: null,
error: action.payload,
};
case AUTH_ACTIONS.LOGOUT:
return {
...state,
isAuthenticated: false,
user: null,
token: null,
isLoading: false,
error: null,
};
case AUTH_ACTIONS.LOAD_USER_START:
return {
...state,
isLoading: true,
};
case AUTH_ACTIONS.LOAD_USER_SUCCESS:
return {
...state,
user: action.payload,
isAuthenticated: true,
isLoading: false,
};
case AUTH_ACTIONS.LOAD_USER_FAILURE:
return {
...state,
user: null,
isAuthenticated: false,
token: null,
isLoading: false,
};
case AUTH_ACTIONS.CLEAR_ERROR:
return {
...state,
error: null,
};
default:
return state;
}
};
// Create context
const AuthContext = createContext();
// Provider component
export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
// Set axios default header
useEffect(() => {
if (state.token) {
axios.defaults.headers.common['Authorization'] = `Bearer ${state.token}`;
} else {
delete axios.defaults.headers.common['Authorization'];
}
}, [state.token]);
// Load user on mount
useEffect(() => {
const loadUser = async () => {
const token = localStorage.getItem('token');
if (token) {
// Set token immediately to prevent white screen
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
dispatch({ type: AUTH_ACTIONS.LOAD_USER_START });
try {
const response = await axios.get('/api/user');
if (response.data.success) {
dispatch({
type: AUTH_ACTIONS.LOAD_USER_SUCCESS,
payload: response.data.user,
});
} else {
dispatch({ type: AUTH_ACTIONS.LOAD_USER_FAILURE });
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
}
} catch (error) {
console.error('Load user error:', error);
dispatch({ type: AUTH_ACTIONS.LOAD_USER_FAILURE });
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
}
} else {
// No token - set loading to false immediately
dispatch({
type: AUTH_ACTIONS.LOAD_USER_SUCCESS,
payload: null,
});
}
};
loadUser();
}, []);
// Login function
const login = async (credentials) => {
try {
dispatch({ type: AUTH_ACTIONS.LOGIN_START });
console.log('🔍 Attempting login to:', '/api/login');
console.log('🔍 Credentials:', { email: credentials.email, password: '***' });
const response = await axios.post('/api/login', credentials);
console.log('🔍 Auth response:', response.data);
console.log('🔍 Response status:', response.status);
if (response.data && response.data.success) {
const { token, user } = response.data;
// Store token
localStorage.setItem('token', token);
// Set axios header
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
dispatch({
type: AUTH_ACTIONS.LOGIN_SUCCESS,
payload: { token, user },
});
return { success: true, data: response.data };
} else {
const message = response.data?.message || 'Login gagal';
console.error('❌ Login failed:', message);
return { success: false, message };
}
} catch (error) {
console.error('❌ Auth error:', error);
console.error('❌ Error config:', error.config);
console.error('❌ Error response:', error.response);
console.error('❌ Error status:', error.response?.status);
console.error('❌ Error data:', error.response?.data);
// Handle different error types
let errorMessage = 'Login gagal. Silakan coba lagi.';
if (error.code === 'ECONNREFUSED' || error.code === 'ERR_NETWORK') {
errorMessage = 'Tidak dapat terhubung ke server. Pastikan backend berjalan.';
} else if (error.response?.status === 401) {
errorMessage = error.response.data?.message || 'Email atau password salah.';
} else if (error.response?.status === 422) {
errorMessage = 'Validasi gagal. Periksa kembali input Anda.';
} else if (error.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (error.message) {
errorMessage = error.message;
}
dispatch({
type: AUTH_ACTIONS.LOGIN_FAILURE,
payload: errorMessage,
});
return { success: false, error: errorMessage };
}
};
// Logout function
const logout = async () => {
try {
if (state.token) {
await axios.post('/api/logout');
}
} catch (error) {
console.error('Logout error:', error);
} finally {
localStorage.removeItem('token');
delete axios.defaults.headers.common['Authorization'];
dispatch({ type: AUTH_ACTIONS.LOGOUT });
}
};
// Clear error
const clearError = () => {
dispatch({ type: AUTH_ACTIONS.CLEAR_ERROR });
};
const value = {
...state,
login,
logout,
clearError,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// Custom hook to use auth context
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export default AuthContext;

View File

@ -1,4 +1,4 @@
/* Tailwind CSS Directives - v3.4.1 */
/* Tailwind CSS Directives - v3.4.1 */
/* eslint-disable-next-line at-rule-no-unknown */
@tailwind base;
/* eslint-disable-next-line at-rule-no-unknown */
@ -8,16 +8,25 @@
/* Custom CSS Variables and Overrides */
:root {
--color-primary: #E6E6FA; /* Lavender */
--color-primary-dark: #D8BFD8; /* Thistle */
--color-accent: #4C6EF5; /* Professional Blue */
--color-primary: #e6e6fa;
/* Refined Lavender/Purple for better visibility */
--color-primary-light: #E9D5FF;
/* Soft Lavender for backgrounds */
--color-primary-dark: #805AD5;
--color-accent: #4C6EF5;
/* Professional Blue */
--color-accent-light: #5C7CFA;
--color-secondary: #495057; /* Professional Gray */
--color-secondary: #495057;
/* Professional Gray */
--color-secondary-light: #6C757D;
--color-danger: #dc2626;
--color-danger-light: #ef4444;
--color-white: #ffffff;
--color-gray-50: #f9fafb;
--color-soft-white: #F8F9FA;
/* Soft White Foundation */
--color-gray-50: #F8F9FA;
/* Matches soft white */
--color-gray-100: #f3f4f6;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
@ -31,13 +40,16 @@
/* Dark Mode Variables */
.dark {
--color-primary: #D8BFD8; /* Darker Lavender */
--color-primary: #D8BFD8;
/* Darker Lavender */
--color-primary-dark: #C8B2DB;
--color-accent: #5C7CFA; /* Lighter Blue for dark mode */
--color-accent: #5C7CFA;
/* Lighter Blue for dark mode */
--color-accent-light: #7C8FFA;
--color-secondary: #6C757D;
--color-secondary-light: #868E96;
--color-white: #1f2937; /* Dark background */
--color-white: #1f2937;
/* Dark background */
--color-gray-50: #374151;
--color-gray-100: #4b5563;
--color-gray-200: #6b7280;
@ -278,10 +290,31 @@ body {
.hover\:bg-green-600:hover,
.hover\:bg-green-700:hover,
.hover\:bg-green-800:hover,
.hover\:bg-green-900:hover {
.hover\:bg-green-900:hover,
.from-green-50,
.from-green-100,
.from-green-200,
.from-green-300,
.from-green-400,
.from-green-500,
.from-green-600,
.from-green-700,
.from-green-800,
.from-green-900,
.to-green-50,
.to-green-100,
.to-green-200,
.to-green-300,
.to-green-400,
.to-green-500,
.to-green-600,
.to-green-700,
.to-green-800,
.to-green-900 {
background-color: transparent !important;
color: inherit !important;
border-color: transparent !important;
background-image: none !important;
}
/* Force remove green from any element */
@ -289,6 +322,7 @@ body {
background-color: transparent !important;
color: inherit !important;
border-color: transparent !important;
background-image: none !important;
}
/* Additional Variables */
@ -331,18 +365,40 @@ body {
background-color: var(--color-gray-50);
}
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.2;
color: var(--color-gray-900);
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: 1.125rem;
}
h6 {
font-size: 1rem;
}
p {
margin-bottom: 1rem;
@ -358,6 +414,61 @@ body {
a:hover {
color: var(--color-primary-dark);
}
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
/* Tailwind CSS Components Layer */
@ -429,20 +540,138 @@ body {
}
@media (max-width: 768px) {
h1 { font-size: 2rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.25rem; }
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.75rem;
}
h3 {
font-size: 1.25rem;
}
/* tailwindcss@3.4.1 - Mobile Responsive Overrides */
.container {
@apply px-3;
}
.btn {
@apply px-5 py-2.5 text-sm;
}
.card {
@apply p-6;
}
}
@layer base {
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
}
/* Blue Highlighter / Stabilo Effect - Per-letter animated */
.highlight-marker {
position: relative;
display: inline;
padding: 2px 6px;
border-radius: 3px;
font-weight: 600;
color: transparent;
background: linear-gradient(to right, #ffffff 50%, #374151 50%);
background-size: 200% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
animation: marker-text-reveal 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--delay, 0) * 1s);
}
.highlight-marker::before {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 0;
height: 100%;
background: rgba(25, 25, 112, 0.85);
border-radius: 3px;
z-index: -1;
animation: marker-sweep 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--delay, 0) * 1s);
}
@keyframes marker-sweep {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes marker-text-reveal {
0% {
background-position: 100% 0;
}
100% {
background-position: 0% 0;
}
}
/* Dark mode */
.dark .highlight-marker {
background: linear-gradient(to right, #ffffff 50%, #d1d5db 50%);
background-size: 200% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.dark .highlight-marker::before {
background: rgba(80, 100, 220, 0.85);
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
.dark ::-webkit-scrollbar-track {
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #191970;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #101050;
}
/* Firefox Scrollbar */
@supports (scrollbar-color: auto) {
html {
scrollbar-width: thin;
scrollbar-color: #191970 #f1f1f1;
}
.dark html {
scrollbar-color: #191970 #1f2937;
}
}

View File

@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

View File

@ -0,0 +1,124 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FiUsers, FiCalendar, FiFileText, FiCheckCircle } from 'react-icons/fi';
const KonselorDashboard = () => {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<h1 className="text-xl font-bold text-gray-900">Dashboard Konselor</h1>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">Konselor</span>
<div className="w-8 h-8 bg-accent rounded-full flex items-center justify-center">
<FiUsers className="w-4 h-4 text-white" />
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<motion.div
className="bg-gradient-to-r from-accent to-primary rounded-2xl p-8 mb-8 text-white"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h2 className="text-2xl font-bold mb-2">Dashboard Konseling</h2>
<p className="opacity-90">
Kelola dan monitoring layanan konseling untuk sivitas akademika
</p>
</motion.div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<motion.div
className="bg-white rounded-xl p-6 shadow-sm"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className="flex items-center justify-between mb-4">
<FiFileText className="w-8 h-8 text-primary" />
<span className="text-2xl font-bold text-gray-900">45</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Total Laporan</h3>
<p className="text-sm text-gray-600">Semua laporan masuk</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<div className="flex items-center justify-between mb-4">
<FiCalendar className="w-8 h-8 text-accent" />
<span className="text-2xl font-bold text-gray-900">8</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Jadwal Hari Ini</h3>
<p className="text-sm text-gray-600">Sesi konseling</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
<div className="flex items-center justify-between mb-4">
<FiCheckCircle className="w-8 h-8 text-green-500" />
<span className="text-2xl font-bold text-gray-900">32</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Selesai</h3>
<p className="text-sm text-gray-600">Laporan selesai</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.3 }}
>
<div className="flex items-center justify-between mb-4">
<FiUsers className="w-8 h-8 text-purple-500" />
<span className="text-2xl font-bold text-gray-900">13</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Sedang Diproses</h3>
<p className="text-sm text-gray-600">Laporan aktif</p>
</motion.div>
</div>
{/* Recent Reports */}
<motion.div
className="bg-white rounded-xl shadow-sm p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Laporan Membutuhkan Perhatian</h3>
<div className="space-y-4">
{[1, 2, 3].map((item) => (
<div key={item} className="flex items-center justify-between p-4 bg-red-50 border border-red-200 rounded-lg">
<div>
<h4 className="font-medium text-gray-900">Kasus #{item}</h4>
<p className="text-sm text-gray-600">Prioritas Tinggi 1 jam lalu</p>
</div>
<button className="px-4 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors">
Lihat Detail
</button>
</div>
))}
</div>
</motion.div>
</div>
</div>
);
};
export default KonselorDashboard;

View File

@ -6,32 +6,28 @@ import Services from '../components/Services';
import Articles from '../components/Articles';
import Contact from '../components/Contact';
import Footer from '../components/Footer';
import { heroService } from '../services/heroService';
import LoginModal from '../components/LoginModal';
const LandingPage = () => {
const [heroData, setHeroData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchHeroData();
}, []);
// Temporary disable API calls to prevent 404 errors
const mockHeroData = {
title: 'Aman Bicara, Aman Melapor',
subtitle: 'Satgas PPKPT Politeknik Negeri Jember',
description: null
};
const fetchHeroData = async () => {
try {
const response = await heroService.get();
if (response.success) {
setHeroData(response.data);
}
} catch (error) {
console.error('Error fetching hero data:', error);
} finally {
setLoading(false);
}
};
setHeroData(mockHeroData);
setLoading(false);
}, []);
return (
<div className="min-h-screen">
<Navbar />
<LoginModal />
<Hero heroData={heroData} />
<About />
<Services />

View File

@ -0,0 +1,147 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FiSettings, FiUsers, FiFileText, FiEdit, FiDatabase } from 'react-icons/fi';
const OperatorDashboard = () => {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<h1 className="text-xl font-bold text-gray-900">Dashboard Operator</h1>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">Operator</span>
<div className="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
<FiSettings className="w-4 h-4 text-white" />
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<motion.div
className="bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl p-8 mb-8 text-white"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h2 className="text-2xl font-bold mb-2">Sistem Administrasi</h2>
<p className="opacity-90">
Kelola seluruh sistem PolijeCare dan monitoring aktivitas
</p>
</motion.div>
{/* Admin Functions */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiUsers className="w-8 h-8 text-blue-500" />
<span className="text-2xl font-bold text-gray-900">156</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Manajemen Pengguna</h3>
<p className="text-sm text-gray-600">Kelola akun pengguna</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiFileText className="w-8 h-8 text-green-500" />
<span className="text-2xl font-bold text-gray-900">89</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Manajemen Pengaduan</h3>
<p className="text-sm text-gray-600">Kelola semua pengaduan</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiEdit className="w-8 h-8 text-purple-500" />
<span className="text-2xl font-bold text-gray-900">24</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Manajemen Artikel</h3>
<p className="text-sm text-gray-600">Kelola konten artikel</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiDatabase className="w-8 h-8 text-orange-500" />
<span className="text-2xl font-bold text-gray-900">12</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Manajemen Kategori</h3>
<p className="text-sm text-gray-600">Kelola kategori laporan</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiSettings className="w-8 h-8 text-red-500" />
<span className="text-sm font-medium text-gray-900">Pengaturan</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Setting Admin</h3>
<p className="text-sm text-gray-600">Konfigurasi sistem</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiFileText className="w-8 h-8 text-indigo-500" />
<span className="text-2xl font-bold text-gray-900">5</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Audit Trail</h3>
<p className="text-sm text-gray-600">Log aktivitas sistem</p>
</motion.div>
</div>
{/* System Status */}
<motion.div
className="bg-white rounded-xl shadow-sm p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Status Sistem</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="font-medium text-green-800 mb-2">Database</h4>
<p className="text-sm text-green-600">Normal 99.9% Uptime</p>
</div>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-800 mb-2">API Server</h4>
<p className="text-sm text-blue-600">Normal Response: 45ms</p>
</div>
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-medium text-yellow-800 mb-2">Storage</h4>
<p className="text-sm text-yellow-600">Warning 85% Used</p>
</div>
</div>
</motion.div>
</div>
</div>
);
};
export default OperatorDashboard;

View File

@ -0,0 +1,120 @@
import React from 'react';
import { motion } from 'framer-motion';
import { FiFileText, FiClock, FiUser, FiPlus } from 'react-icons/fi';
const UserDashboard = () => {
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<h1 className="text-xl font-bold text-gray-900">Dashboard Mahasiswa</h1>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600">Mahasiswa</span>
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center">
<FiUser className="w-4 h-4 text-white" />
</div>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<motion.div
className="bg-gradient-to-r from-primary to-accent rounded-2xl p-8 mb-8 text-white"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<h2 className="text-2xl font-bold mb-2">Selamat Datang di PolijeCare</h2>
<p className="opacity-90">
Portal layanan perlindungan dan kesejahteraan sivitas akademika Politeknik Negeri Jember
</p>
</motion.div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiPlus className="w-8 h-8 text-primary" />
<span className="text-2xl font-bold text-gray-900">+</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Buat Laporan</h3>
<p className="text-sm text-gray-600">Ajukan pengaduan atau konseling</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiFileText className="w-8 h-8 text-accent" />
<span className="text-2xl font-bold text-gray-900">12</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Riwayat Laporan</h3>
<p className="text-sm text-gray-600">Lihat semua laporan Anda</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiClock className="w-8 h-8 text-green-500" />
<span className="text-2xl font-bold text-gray-900">3</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Sedang Diproses</h3>
<p className="text-sm text-gray-600">Laporan dalam proses</p>
</motion.div>
<motion.div
className="bg-white rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="flex items-center justify-between mb-4">
<FiUser className="w-8 h-8 text-purple-500" />
<span className="text-sm font-medium text-primary">Profil</span>
</div>
<h3 className="font-semibold text-gray-900 mb-2">Pengaturan</h3>
<p className="text-sm text-gray-600">Kelola akun Anda</p>
</motion.div>
</div>
{/* Recent Reports */}
<motion.div
className="bg-white rounded-xl shadow-sm p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h3 className="text-lg font-semibold text-gray-900 mb-4">Laporan Terbaru</h3>
<div className="space-y-4">
{[1, 2, 3].map((item) => (
<div key={item} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<h4 className="font-medium text-gray-900">Laporan #{item}</h4>
<p className="text-sm text-gray-600">Dibuat 2 hari yang lalu</p>
</div>
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 text-sm rounded-full">
Sedang Diproses
</span>
</div>
))}
</div>
</motion.div>
</div>
</div>
);
};
export default UserDashboard;

View File

@ -0,0 +1,50 @@
import axios from 'axios';
// Create axios instance
const api = axios.create({
baseURL: 'http://localhost:8000', // Backend URL
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for error handling
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// Handle 401 Unauthorized - only redirect if not already on home page
if (error.response?.status === 401 && window.location.pathname !== '/') {
localStorage.removeItem('token');
delete api.defaults.headers.common['Authorization'];
// Use navigate instead of direct location change to avoid infinite loop
console.log('🔐 Token expired, redirecting to home...');
}
// Handle 403 Forbidden
if (error.response?.status === 403) {
console.error('Access forbidden - insufficient permissions');
}
return Promise.reject(error);
}
);
export default api;

View File

@ -4,77 +4,119 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
darkMode: ['class', "class"],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#E6E6FA',
dark: '#D8BFD8',
light: '#F0E6FF',
},
accent: {
DEFAULT: '#4C6EF5',
light: '#5C7CFA',
dark: '#364FC7',
},
secondary: {
DEFAULT: '#495057',
light: '#6C757D',
dark: '#343A40',
},
danger: {
DEFAULT: '#DC2626',
light: '#EF4444',
dark: '#B91C1C',
},
gray: {
50: '#F5F7FA',
100: '#E5E7EB',
200: '#D1D5DB',
300: '#9CA3AF',
400: '#6B7280',
500: '#4B5563',
600: '#374151',
700: '#1F2937',
800: '#111827',
900: '#030712',
},
purple: {
50: '#F3E8FF',
100: '#E9D5FF',
200: '#D8B4FE',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
spacing: {
'18': '4.5rem',
'80': '20rem',
'88': '22rem',
},
borderRadius: {
'xl': '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
},
boxShadow: {
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'card': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'card-hover': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
},
animation: {
'float': 'float 3s ease-in-out infinite',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
},
},
extend: {
colors: {
primary: {
DEFAULT: 'hsl(var(--primary))',
dark: '#D8BFD8',
light: '#F0E6FF',
foreground: 'hsl(var(--primary-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
light: '#5C7CFA',
dark: '#364FC7',
foreground: 'hsl(var(--accent-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
light: '#6C757D',
dark: '#343A40',
foreground: 'hsl(var(--secondary-foreground))'
},
danger: {
DEFAULT: '#DC2626',
light: '#EF4444',
dark: '#B91C1C'
},
gray: {
'50': '#F5F7FA',
'100': '#E5E7EB',
'200': '#D1D5DB',
'300': '#9CA3AF',
'400': '#6B7280',
'500': '#4B5563',
'600': '#374151',
'700': '#1F2937',
'800': '#111827',
'900': '#030712'
},
purple: {
'50': '#F3E8FF',
'100': '#E9D5FF',
'200': '#D8B4FE'
},
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
fontFamily: {
sans: [
'Inter',
'system-ui',
'sans-serif'
]
},
spacing: {
'18': '4.5rem',
'80': '20rem',
'88': '22rem'
},
borderRadius: {
xl: '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
boxShadow: {
soft: '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
card: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'card-hover': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
},
animation: {
float: 'float 3s ease-in-out infinite',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite'
},
keyframes: {
float: {
'0%, 100%': {
transform: 'translateY(0px)'
},
'50%': {
transform: 'translateY(-10px)'
}
}
}
}
},
plugins: [],
plugins: [require("tailwindcss-animate")],
}

View File

@ -1,7 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path"
import { fileURLToPath } from "url"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})