Initial commit ta-project
This commit is contained in:
commit
d4ecbe5b45
|
|
@ -0,0 +1,258 @@
|
|||
# PolijeCare - Sistem Pengaduan Satgas PPKPT Politeknik Negeri Jember
|
||||
|
||||
Sistem pengaduan modern untuk Satgas PPKPT Politeknik Negeri Jember dengan desain clean, akademik, dan profesional.
|
||||
|
||||
## 🏗️ Project Structure
|
||||
|
||||
```
|
||||
polije-care/
|
||||
├── backend/ # Laravel API
|
||||
│ ├── app/
|
||||
│ │ ├── Models/ # Eloquent Models
|
||||
│ │ └── Http/Controllers/Api/
|
||||
│ ├── database/
|
||||
│ │ ├── migrations/ # Database migrations
|
||||
│ │ └── seeders/ # Database seeders
|
||||
│ └── routes/api.php # API routes
|
||||
└── frontend/ # React Vite App
|
||||
├── src/
|
||||
│ ├── api/ # Axios configuration
|
||||
│ ├── components/ # React components
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ ├── pages/ # Page components
|
||||
│ ├── services/ # API services
|
||||
│ └── utils/ # Utility functions
|
||||
└── .env # Environment variables
|
||||
```
|
||||
|
||||
## 🚀 Setup Instructions
|
||||
|
||||
### Backend Setup (Laravel)
|
||||
|
||||
1. **Navigate to backend directory**
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
3. **Copy environment file**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. **Generate application key**
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
5. **Configure database**
|
||||
- Edit `.env` file
|
||||
- Set your database credentials
|
||||
|
||||
6. **Run migrations and seeders**
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan db:seed
|
||||
```
|
||||
|
||||
7. **Start development server**
|
||||
```bash
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
API will be available at `http://127.0.0.1:8000`
|
||||
|
||||
### Frontend Setup (React)
|
||||
|
||||
1. **Navigate to frontend directory**
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start development server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend will be available at `http://localhost:5173`
|
||||
|
||||
## 📡 API Endpoints
|
||||
|
||||
### Articles
|
||||
- `GET /api/articles` - Get all articles
|
||||
- `GET /api/articles/{slug}` - Get article by slug
|
||||
|
||||
### Contact
|
||||
- `GET /api/contact` - Get contact information
|
||||
|
||||
### Hero Section
|
||||
- `GET /api/hero` - Get hero section content
|
||||
|
||||
## 🎨 Features
|
||||
|
||||
### ✅ Implemented Features
|
||||
|
||||
1. **Modern Landing Page**
|
||||
- Hero section with call-to-action buttons
|
||||
- About section with animated elements
|
||||
- Services section (Cara Melapor)
|
||||
- Articles & Announcements section
|
||||
- Contact section with multiple contact methods
|
||||
- Professional footer
|
||||
|
||||
2. **Navigation & Authentication**
|
||||
- Responsive navbar with mobile menu
|
||||
- Authentication logic with role-based redirects
|
||||
- Smooth scroll navigation
|
||||
- Mixed routing (anchors + React Router)
|
||||
|
||||
3. **Design & Animations**
|
||||
- Framer Motion animations on scroll
|
||||
- Modern UI with Tailwind-inspired CSS
|
||||
- Responsive design for all screen sizes
|
||||
- Academic color scheme (navy + green + white)
|
||||
- Hover effects and micro-interactions
|
||||
|
||||
4. **Data Integration**
|
||||
- Axios configuration with interceptors
|
||||
- API services for all data fetching
|
||||
- Loading states and error handling
|
||||
- Environment variable configuration
|
||||
|
||||
### 🎯 Key Components
|
||||
|
||||
- **Navbar**: Fixed navigation with auth logic
|
||||
- **Hero**: Dynamic hero section from API
|
||||
- **About**: Information about Satgas PPKPT
|
||||
- **Services**: Two reporting methods (WhatsApp + Form)
|
||||
- **Articles**: Dynamic articles from API
|
||||
- **Contact**: Complete contact information
|
||||
- **Footer**: Professional footer with links
|
||||
|
||||
## 🔧 Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Laravel 12** - PHP Framework
|
||||
- **MySQL** - Database
|
||||
- **Sanctum** - API Authentication (ready for future use)
|
||||
|
||||
### Frontend
|
||||
- **React 19** - UI Library
|
||||
- **Vite** - Build Tool
|
||||
- **React Router DOM** - Routing
|
||||
- **Axios** - HTTP Client
|
||||
- **Framer Motion** - Animations
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
### Colors
|
||||
- Primary: Navy Blue (`#1e3a8a`)
|
||||
- Accent: Green Campus (`#16a34a`)
|
||||
- Danger: Red (`#dc2626`)
|
||||
- Background: Soft Gray (`#f9fafb`)
|
||||
|
||||
### Typography
|
||||
- Font Family: Inter, system-ui
|
||||
- Clean, modern, academic style
|
||||
|
||||
### Components
|
||||
- Rounded corners (`--radius-xl`)
|
||||
- Soft shadows (`--shadow-md`, `--shadow-lg`)
|
||||
- Hover animations
|
||||
- Responsive grid layouts
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
- Mobile-first approach
|
||||
- Breakpoints: sm (640px), md (768px), lg (1024px)
|
||||
- Adaptive navigation (mobile menu)
|
||||
- Flexible grid layouts
|
||||
- Touch-friendly buttons
|
||||
|
||||
## 🔐 Authentication Flow
|
||||
|
||||
The system includes authentication logic for future implementation:
|
||||
|
||||
1. **Unauthenticated Users**
|
||||
- See "Masuk" button
|
||||
- Redirect to `/login`
|
||||
|
||||
2. **Authenticated Users**
|
||||
- See "Dashboard" and "Keluar" buttons
|
||||
- Role-based redirects:
|
||||
- `user` → `/user/dashboard`
|
||||
- `operator` → `/operator/dashboard`
|
||||
- `konselor` → `/konselor/dashboard`
|
||||
- default → `/redirect`
|
||||
|
||||
## 🌐 Environment Variables
|
||||
|
||||
### Frontend (.env)
|
||||
```env
|
||||
VITE_API_URL=http://127.0.0.1:8000/api
|
||||
```
|
||||
|
||||
### Backend (.env)
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=polije_care
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
```
|
||||
|
||||
## 📝 Sample Data
|
||||
|
||||
The database seeder includes:
|
||||
|
||||
1. **Hero Section**
|
||||
- Title: "Aman Bicara, Aman Melapor"
|
||||
- Subtitle: "Satgas PPKPT Politeknik Negeri Jember"
|
||||
|
||||
2. **Contact Information**
|
||||
- Address, Phone, Email, Instagram
|
||||
|
||||
3. **Sample Articles**
|
||||
- 3 sample articles with titles and content
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Backend Deployment
|
||||
1. Configure production database
|
||||
2. Set `APP_ENV=production`
|
||||
3. Run `php artisan config:cache`
|
||||
4. Set up web server (Apache/Nginx)
|
||||
|
||||
### Frontend Deployment
|
||||
1. Run `npm run build`
|
||||
2. Deploy `dist/` folder to web server
|
||||
3. Configure environment variables
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create feature branch
|
||||
3. Commit changes
|
||||
4. Push to branch
|
||||
5. Create Pull Request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is proprietary to Politeknik Negeri Jember.
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For support, contact:
|
||||
- Email: satgasppkpt@polije.ac.id
|
||||
- Phone: +62 331-123456
|
||||
- Instagram: @satgasppkpt_polije
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ArticleResource;
|
||||
use App\Models\Article;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ArticleController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of articles.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$articles = Article::latest()->published()->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Articles retrieved successfully',
|
||||
'data' => ArticleResource::collection($articles),
|
||||
'meta' => [
|
||||
'total' => $articles->count(),
|
||||
'last_updated' => now()->toISOString()
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve articles',
|
||||
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified article.
|
||||
*/
|
||||
public function show(string $slug): JsonResponse
|
||||
{
|
||||
try {
|
||||
$article = Article::where('slug', $slug)->published()->first();
|
||||
|
||||
if (!$article) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Article not found',
|
||||
'data' => null
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Article retrieved successfully',
|
||||
'data' => new ArticleResource($article)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve article',
|
||||
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ContactInfoResource;
|
||||
use App\Models\ContactInfo;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ContactController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the contact information.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$contact = ContactInfo::first();
|
||||
|
||||
if (!$contact) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Contact information not found',
|
||||
'data' => null
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Contact information retrieved successfully',
|
||||
'data' => new ContactInfoResource($contact)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to retrieve contact information',
|
||||
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\HeroSection;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HeroController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$hero = HeroSection::first();
|
||||
|
||||
if (!$hero) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Hero section not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $hero
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ArticleResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'slug' => $this->slug,
|
||||
'image' => $this->image ? asset('storage/' . $this->image) : null,
|
||||
'excerpt' => \Illuminate\Support\Str::limit(strip_tags($this->content), 150),
|
||||
'content' => $this->content,
|
||||
'published_at' => $this->created_at->format('d M Y'),
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ContactInfoResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'address' => $this->address,
|
||||
'phone' => $this->phone,
|
||||
'email' => $this->email,
|
||||
'instagram' => $this->instagram,
|
||||
'whatsapp' => $this->whatsapp ?? '+6281234567890',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Article extends Model
|
||||
{
|
||||
use HasFactory, HasUuids;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'image',
|
||||
'content',
|
||||
'is_published',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_published' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model.
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($article) {
|
||||
if (empty($article->slug)) {
|
||||
$article->slug = Str::slug($article->title);
|
||||
}
|
||||
});
|
||||
|
||||
static::updating(function ($article) {
|
||||
if ($article->isDirty('title') && empty($article->slug)) {
|
||||
$article->slug = Str::slug($article->title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only published articles.
|
||||
*/
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('is_published', true)
|
||||
->where('published_at', '<=', now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route key for the model.
|
||||
*/
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the excerpt attribute.
|
||||
*/
|
||||
public function getExcerptAttribute(): string
|
||||
{
|
||||
return Str::limit(strip_tags($this->content), 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted published date.
|
||||
*/
|
||||
public function getFormattedPublishedAtAttribute(): string
|
||||
{
|
||||
return $this->published_at->format('d M Y');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContactInfo extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'address',
|
||||
'phone',
|
||||
'email',
|
||||
'instagram',
|
||||
'whatsapp',
|
||||
'facebook',
|
||||
'twitter',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the formatted phone number.
|
||||
*/
|
||||
public function getFormattedPhoneAttribute(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WhatsApp link.
|
||||
*/
|
||||
public function getWhatsappLinkAttribute(): string
|
||||
{
|
||||
$phone = preg_replace('/[^0-9]/', '', $this->whatsapp ?? $this->phone);
|
||||
return "https://wa.me/{$phone}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Instagram link.
|
||||
*/
|
||||
public function getInstagramLinkAttribute(): string
|
||||
{
|
||||
$username = ltrim($this->instagram, '@');
|
||||
return "https://instagram.com/{$username}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HeroSection extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'subtitle',
|
||||
'description',
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your settings for cross-origin resource sharing
|
||||
| or "CORS". This determines what cross-origin operations may execute
|
||||
| in web browsers. You are free to adjust these settings as needed.
|
||||
|
|
||||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175'],
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
'supports_credentials' => false,
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?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('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?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('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?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('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('articles', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('image')->nullable();
|
||||
$table->text('content');
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['is_published', 'published_at']);
|
||||
$table->index('slug');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('articles');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('contact_infos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->text('address');
|
||||
$table->string('phone');
|
||||
$table->string('email');
|
||||
$table->string('instagram');
|
||||
$table->string('whatsapp')->nullable();
|
||||
$table->string('facebook')->nullable();
|
||||
$table->string('twitter')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('contact_infos');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('hero_sections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title');
|
||||
$table->string('subtitle');
|
||||
$table->text('description');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('hero_sections');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?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('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Article;
|
||||
use App\Models\ContactInfo;
|
||||
use App\Models\HeroSection;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// Hero Section
|
||||
HeroSection::create([
|
||||
'title' => 'Aman Bicara, Aman Melapor',
|
||||
'subtitle' => 'Satgas PPKPT Politeknik Negeri Jember',
|
||||
'description' => 'Kami siap mendengar dan membantu Anda dengan profesionalisme dan kerahasiaan terjamin. Setiap laporan akan ditangani dengan empati dan seksama.'
|
||||
]);
|
||||
|
||||
// Contact Info
|
||||
ContactInfo::create([
|
||||
'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'
|
||||
]);
|
||||
|
||||
// Professional Articles
|
||||
$articles = [
|
||||
[
|
||||
'title' => 'Pentingnya Menjaga Lingkungan Kampus Aman dari Kekerasan Seksual',
|
||||
'slug' => 'pentingnya-menjaga-lingkungan-kampus-aman-dari-kekerasan-seksual',
|
||||
'image' => 'articles/safe-campus.jpg',
|
||||
'content' => '<p>Lingkungan kampus yang aman adalah hak setiap sivitas akademika. Satgas PPKPT Politeknik Negeri Jember berkomitmen untuk menciptakan suasana belajar yang bebas dari kekerasan seksual.</p><p>Kekerasan seksual dalam lingkungan kampus dapat berupa berbagai bentuk, mulai dari pelecehan verbal hingga kekerasan fisik. Penting bagi kita semua untuk mengenali tanda-tanda dan bagaimana cara melaporkannya.</p><p>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>',
|
||||
'is_published' => true,
|
||||
'published_at' => now()->subDays(7),
|
||||
],
|
||||
[
|
||||
'title' => 'Prosedur Pelaporan Kasus Kekerasan Seksual di Polije',
|
||||
'slug' => 'prosedur-pelaporan-kasus-kekerasan-seksual-di-polije',
|
||||
'image' => 'articles/reporting-procedure.jpg',
|
||||
'content' => '<p>Prosedur pelaporan kasus kekerasan seksual di Politeknik Negeri Jember dirancang untuk memberikan kemudahan dan rasa aman bagi pelapor.</p><p><strong>Langkah 1:</strong> Hubungi Satgas PPKPT melalui WhatsApp atau datang langsung ke sekretariat.</p><p><strong>Langkah 2:</strong> Tim kami akan melakukan pendengaran awal dan memberikan informasi tentang hak-hak Anda.</p><p><strong>Langkah 3:</strong> Jika Anda setuju, kami akan membantu proses pelaporan formal.</p><p><strong>Langkah 4:</strong> Pendampingan psikologis dan hukum akan disediakan selama proses berlangsung.</p><p>Semua proses dijamin kerahasiaannya dan tidak akan ada diskriminasi terhadap pelapor.</p>',
|
||||
'is_published' => true,
|
||||
'published_at' => now()->subDays(5),
|
||||
],
|
||||
[
|
||||
'title' => 'Hak dan Kewajiban Korban dan Pelapor Kekerasan Seksual',
|
||||
'slug' => 'hak-dan-kewajiban-korban-dan-pelapor-kekerasan-seksual',
|
||||
'image' => 'articles/rights-responsibilities.jpg',
|
||||
'content' => '<p>Sebagai korban atau pelapor kekerasan seksual, Anda memiliki hak-hak yang dilindungi oleh undang-undang dan peraturan institusi.</p><p><strong>Hak-hak Anda:</strong></p><ul><li>Hak atas perlindungan dan keamanan</li><li>Hak atas kerahasiaan identitas</li><li>Hak mendapatkan pendampingan psikologis</li><li>Hak mendapatkan bantuan hukum</li><li>Hak bebas dari intimidasi dan ancaman</li></ul><p><strong>Kewajiban Anda:</strong></p><ul><li>Memberikan informasi yang akurat</li><li>Kooperatif dalam proses penanganan</li><li>Menghormati proses yang berlaku</li></ul>',
|
||||
'is_published' => true,
|
||||
'published_at' => now()->subDays(3),
|
||||
],
|
||||
[
|
||||
'title' => 'Peran Satgas PPKPT dalam Mencegah Kekerasan Seksual',
|
||||
'slug' => 'peran-satgas-ppkpt-dalam-mencegah-kekerasan-seksual',
|
||||
'image' => 'articles/prevention-role.jpg',
|
||||
'content' => '<p>Satgas PPKPT Politeknik Negeri Jember memiliki peran penting dalam pencegahan dan penanganan kekerasan seksual di lingkungan kampus.</p><p><strong>Program Pencegahan:</strong></p><ul><li>Edukasi dan sosialisasi kepada sivitas akademika</li><li>Workshop dan pelatihan tentang konsen seksual</li><li>Kampanye anti-kekerasan seksual</li><li>Pembuatan kebijakan dan regulasi</li></ul><p><strong>Program Penanganan:</strong></p><ul><li>Penerimaan dan verifikasi laporan</li><li>Investigasi internal yang profesional</li><li>Pendampingan korban</li><li>Rekomendasi sanksi jika terbukti</li></ul>',
|
||||
'is_published' => true,
|
||||
'published_at' => now()->subDays(1),
|
||||
],
|
||||
[
|
||||
'title' => 'Dukungan Psikologis bagi Korban Kekerasan Seksual',
|
||||
'slug' => 'dukungan-psikologis-bagi-korban-kekerasan-seksual',
|
||||
'image' => 'articles/psychological-support.jpg',
|
||||
'content' => '<p>Dampak psikologis kekerasan seksual dapat sangat berat bagi korban. Satgas PPKPT menyediakan layanan dukungan psikologis profesional.</p><p><strong>Layanan yang Tersedia:</strong></p><ul><li>Konseling individu dengan psikolog berpengalaman</li><li>Terapi kelompok jika diperlukan</li><li>Referensi ke psikiater jika ada indikasi medis</li><li>Dukungan selama proses hukum</li></ul><p><strong>Tujuan Dukungan:</strong></p><ul><li>Membantu korban memproses trauma</li><li>Membangun kembali kepercayaan diri</li><li>Memberikan coping strategies</li><li>Mencegah long-term psychological impact</li></ul>',
|
||||
'is_published' => true,
|
||||
'published_at' => now(),
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($articles as $article) {
|
||||
Article::create($article);
|
||||
}
|
||||
|
||||
// Create test user
|
||||
User::factory()->create([
|
||||
'name' => 'Admin PolijeCare',
|
||||
'email' => 'admin@polije.ac.id',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import './bootstrap';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
|
||||
Route::get('/test', function () {
|
||||
return response()->json([
|
||||
'status' => 'API OK',
|
||||
]);
|
||||
});
|
||||
|
||||
// Articles
|
||||
Route::get('/articles', [ArticleController::class, 'index']);
|
||||
Route::get('/articles/{slug}', [ArticleController::class, 'show']);
|
||||
|
||||
// Contact
|
||||
Route::get('/contact', [ContactController::class, 'index']);
|
||||
|
||||
// Hero Section
|
||||
Route::get('/hero', [HeroController::class, 'index']);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
});
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
VITE_API_URL=http://127.0.0.1:8000/api
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/logo_polijecare.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Polijecare - Satgas PPKPT Politeknik Negeri Jember</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.5",
|
||||
"framer-motion": "^10.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"tailwindcss": "^3.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"postcss": "^8.5.6",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background Circle -->
|
||||
<circle cx="100" cy="100" r="95" fill="#E6E6FA" stroke="#4C6EF5" stroke-width="3"/>
|
||||
|
||||
<!-- Shield Shape -->
|
||||
<path d="M100 30L60 50V90C60 120 100 150 100 150C100 150 140 120 140 90V50L100 30Z" fill="#4C6EF5"/>
|
||||
|
||||
<!-- Heart in Shield -->
|
||||
<path d="M100 65C92 57 78 57 78 70C78 80 100 100 100 100C100 100 122 80 122 70C122 57 108 57 100 65Z" fill="#E6E6FA"/>
|
||||
|
||||
<!-- Text -->
|
||||
<text x="100" y="180" text-anchor="middle" fill="#495057" font-family="Arial, sans-serif" font-size="12" font-weight="bold">
|
||||
POLIJE CARE
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 675 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,38 @@
|
|||
#root {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: box;
|
||||
-webkit-line-clamp: 2;
|
||||
-moz-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: box;
|
||||
-webkit-line-clamp: 3;
|
||||
-moz-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
-moz-box-orient: vertical;
|
||||
box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import LandingPage from './pages/LandingPage';
|
||||
import ArticleDetail from './components/ArticleDetail';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<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 />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import axios from "axios";
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://127.0.0.1:8000/api';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
withCredentials: false, // Changed to false to fix CORS
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// Add auth token if available
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Handle unauthorized
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,239 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { fadeIn, slideUp, slideLeft, staggerChildren } from '../utils/motionVariants';
|
||||
|
||||
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;
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { articleService } from '../services/articleService';
|
||||
import { fadeIn, slideUp } from '../utils/motionVariants';
|
||||
|
||||
const ArticleDetail = () => {
|
||||
const { slug } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [article, setArticle] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchArticle();
|
||||
}, [slug]);
|
||||
|
||||
const fetchArticle = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await articleService.getBySlug(slug);
|
||||
if (response.success) {
|
||||
setArticle(response.data);
|
||||
} else {
|
||||
setError('Artikel tidak ditemukan');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Gagal memuat artikel');
|
||||
console.error('Error fetching article:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return new Date(dateString).toLocaleDateString('id-ID', options);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-primary/5 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p className="text-gray-600 font-medium">Memuat artikel...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !article) {
|
||||
return (
|
||||
<div className="min-h-screen bg-primary/5 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-red-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Artikel Tidak Ditemukan</h2>
|
||||
<p className="text-gray-600 mb-6">{error || 'Artikel yang Anda cari tidak tersedia.'}</p>
|
||||
<button
|
||||
onClick={() => navigate('/#articles')}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Kembali ke Artikel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary/5">
|
||||
{/* Navigation Breadcrumb */}
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center h-16 space-x-2 text-sm">
|
||||
<Link to="/" className="text-gray-500 hover:text-primary transition-colors">
|
||||
Beranda
|
||||
</Link>
|
||||
<span className="text-gray-400">/</span>
|
||||
<Link to="/#articles" className="text-gray-500 hover:text-primary transition-colors">
|
||||
Artikel
|
||||
</Link>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span className="text-gray-900 font-medium truncate">{article.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Article Content */}
|
||||
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="bg-white rounded-3xl shadow-soft overflow-hidden"
|
||||
>
|
||||
{/* Article Header */}
|
||||
<div className="relative h-64 sm:h-80 lg:h-96">
|
||||
<img
|
||||
src={article.image || `https://picsum.photos/seed/${article.slug}/800/400.jpg`}
|
||||
alt={article.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-0 left-0 right-0 p-8">
|
||||
<motion.div
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<span className="px-3 py-1 bg-accent text-white text-sm font-medium rounded-full">
|
||||
{article.category || 'Artikel'}
|
||||
</span>
|
||||
<span className="text-white/90 text-sm">
|
||||
{formatDate(article.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4 leading-tight">
|
||||
{article.title}
|
||||
</h1>
|
||||
<div className="flex items-center space-x-4 text-white/90 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>{article.author || 'Admin PolijeCare'}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="w-4 h-4 text-white/90" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{article.read_time || '5'} menit baca</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Body */}
|
||||
<div className="p-8 lg:p-12">
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.4 }}
|
||||
className="prose prose-lg max-w-none"
|
||||
>
|
||||
{/* Article Summary */}
|
||||
{article.excerpt && (
|
||||
<div className="bg-primary/10 border-l-4 border-primary p-6 rounded-r-xl mb-8">
|
||||
<p className="text-gray-700 italic text-lg leading-relaxed">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article Content */}
|
||||
<div
|
||||
className="text-gray-700 leading-relaxed space-y-6"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: article.content || article.body || `
|
||||
<p>Konten artikel sedang dalam proses pengembangan. Artikel ini akan menampilkan informasi lengkap mengenai topik yang dibahas.</p>
|
||||
<p>PolijeCare berkomitmen untuk menyediakan informasi yang akurat dan bermanfaat bagi sivitas akademika Politeknik Negeri Jember.</p>
|
||||
`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Article Tags */}
|
||||
{article.tags && article.tags.length > 0 && (
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tag</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{article.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 bg-gray-100 text-gray-700 text-sm rounded-full hover:bg-primary hover:text-white transition-colors cursor-pointer"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Share Section */}
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.6 }}
|
||||
className="mt-12 pt-8 border-t border-gray-200"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-gray-600 font-medium">Bagikan artikel:</span>
|
||||
<div className="flex space-x-3">
|
||||
<button className="w-10 h-10 bg-blue-500 text-white rounded-lg flex items-center justify-center hover:bg-blue-600 transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="w-10 h-10 bg-sky-500 text-white rounded-lg flex items-center justify-center hover:bg-sky-600 transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="w-10 h-10 bg-green-500 text-white rounded-lg flex items-center justify-center hover:bg-green-600 transition-colors">
|
||||
<svg className="w-5 h-5" 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/#articles')}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Kembali ke Daftar Artikel
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Related Articles */}
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.8 }}
|
||||
className="mt-12"
|
||||
>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Artikel Terkait</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Placeholder for related articles */}
|
||||
{[1, 2, 3].map((item) => (
|
||||
<div key={item} className="bg-white rounded-xl shadow-soft overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="h-48 bg-gradient-to-br from-primary/20 to-accent/20"></div>
|
||||
<div className="p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Artikel Terkait {item}</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">Lorem ipsum dolor sit amet consectetur adipiscing elit.</p>
|
||||
<button className="text-accent font-medium text-sm hover:text-accent-dark transition-colors">
|
||||
Baca Selengkapnya →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArticleDetail;
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
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';
|
||||
|
||||
const Articles = () => {
|
||||
const [articles, setArticles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchArticles();
|
||||
}, []);
|
||||
|
||||
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 formatDate = (dateString) => {
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
return new Date(dateString).toLocaleDateString('id-ID', options);
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<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 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Articles;
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { contactService } from '../services/contactService';
|
||||
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
|
||||
|
||||
const Contact = () => {
|
||||
const [contactInfo, setContactInfo] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchContactInfo();
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultContact = {
|
||||
address: 'Jl. Mastrip PO Box 164, Jember 68121, Jawa Timur, Indonesia',
|
||||
phone: '+62 331-123456',
|
||||
email: 'satgasppkpt@polije.ac.id',
|
||||
instagram: '@satgasppkpt_polije',
|
||||
whatsapp: '+62 812-3456-7890'
|
||||
};
|
||||
|
||||
const contact = contactInfo || defaultContact;
|
||||
|
||||
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',
|
||||
value: contact.phone,
|
||||
href: `tel:${contact.phone}`,
|
||||
color: 'text-primary'
|
||||
},
|
||||
{
|
||||
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',
|
||||
value: contact.email,
|
||||
href: `mailto:${contact.email}`,
|
||||
color: 'text-primary'
|
||||
},
|
||||
{
|
||||
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',
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
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>
|
||||
</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 }}
|
||||
>
|
||||
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>
|
||||
</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={`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>
|
||||
<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}
|
||||
>
|
||||
{method.value}
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { fadeIn } from '../utils/motionVariants';
|
||||
|
||||
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 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'
|
||||
}
|
||||
];
|
||||
|
||||
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>
|
||||
|
||||
<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"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
|
||||
{/* 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">
|
||||
<motion.a
|
||||
href="https://wa.me/6281234567890"
|
||||
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 }}
|
||||
>
|
||||
<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>
|
||||
</motion.a>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { fadeIn, slideUp, slideLeft, slideRight } from '../utils/motionVariants';
|
||||
|
||||
const Hero = ({ heroData }) => {
|
||||
const defaultHero = {
|
||||
title: 'Aman Bicara, Aman Melapor',
|
||||
subtitle: 'Satgas PPKPT Politeknik Negeri Jember',
|
||||
description: 'Kami siap mendengar dan membantu Anda dengan profesionalisme dan kerahasiaan terjamin.'
|
||||
};
|
||||
|
||||
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"
|
||||
>
|
||||
{/* 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={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.3, 0.5, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute bottom-20 right-10 w-96 h-96 bg-primary/10 rounded-full blur-3xl"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.3, 0.4, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 2
|
||||
}}
|
||||
/>
|
||||
</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">
|
||||
{/* Left Content */}
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<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"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
{hero.title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.h2
|
||||
className="text-xl md:text-2xl lg:text-3xl font-semibold text-primary dark:text-primary-light"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
{hero.subtitle}
|
||||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed max-w-xl"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
{hero.description}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col sm:flex-row gap-4 pt-4"
|
||||
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"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.7 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Butuh Bantuan Darurat
|
||||
</motion.a>
|
||||
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.8 }}
|
||||
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>
|
||||
</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
|
||||
className="relative lg:pl-12"
|
||||
variants={slideRight}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
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"
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</motion.div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
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';
|
||||
|
||||
const Navbar = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const { isAuthenticated, user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const navLinks = [
|
||||
{ name: 'Beranda', href: '#hero' },
|
||||
{ name: 'Tentang Kami', href: '#about' },
|
||||
{ name: 'Cara Melapor', href: '#services' },
|
||||
{ name: 'Artikel', href: '#articles' },
|
||||
{ name: 'Kontak', href: '#contact' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleDashboardRedirect = () => {
|
||||
if (!user) return;
|
||||
|
||||
switch (user.role) {
|
||||
case 'user':
|
||||
navigate('/user/dashboard');
|
||||
break;
|
||||
case 'operator':
|
||||
navigate('/operator/dashboard');
|
||||
break;
|
||||
case 'konselor':
|
||||
navigate('/konselor/dashboard');
|
||||
break;
|
||||
default:
|
||||
navigate('/redirect');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
{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>
|
||||
</>
|
||||
) : (
|
||||
<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"
|
||||
>
|
||||
Masuk
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
|
@ -0,0 +1,250 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
|
||||
|
||||
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'
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
];
|
||||
|
||||
const importantInfo = [
|
||||
{
|
||||
icon: '🛡️',
|
||||
title: 'Aman',
|
||||
description: 'Identitas Anda akan dirahasiakan sepenuhnya'
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
title: 'Profesional',
|
||||
description: 'Ditangani oleh tim yang berpengalaman'
|
||||
},
|
||||
{
|
||||
icon: '💚',
|
||||
title: 'Support',
|
||||
description: 'Dapatkan pendampingan penuh dari kami'
|
||||
}
|
||||
];
|
||||
|
||||
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>
|
||||
</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"
|
||||
variants={staggerChildren}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{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 }}
|
||||
>
|
||||
{/* 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 }}
|
||||
>
|
||||
{method.icon}
|
||||
</motion.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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
<div className="pt-6">
|
||||
{method.buttonLink.startsWith('http') ? (
|
||||
<a
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Services;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
const SimpleThemeToggle = () => {
|
||||
const handleToggle = () => {
|
||||
const root = document.documentElement;
|
||||
const isDark = root.classList.contains('dark');
|
||||
|
||||
console.log('Current dark mode:', isDark);
|
||||
|
||||
if (isDark) {
|
||||
root.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
console.log('Switched to light mode');
|
||||
} else {
|
||||
root.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
console.log('Switched to dark mode');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
Toggle Theme (Simple)
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleThemeToggle;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const ThemeTest = () => {
|
||||
const { theme, toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<p className="text-gray-900 dark:text-white">
|
||||
Current theme: {theme}
|
||||
</p>
|
||||
<p className="text-gray-900 dark:text-white">
|
||||
Is dark: {isDark.toString()}
|
||||
</p>
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="mt-2 px-4 py-2 bg-primary text-white rounded"
|
||||
>
|
||||
Toggle Theme
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeTest;
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const { theme, toggleTheme, isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
onClick={toggleTheme}
|
||||
className="relative w-14 h-7 bg-gray-200 dark:bg-gray-700 rounded-full p-1 transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{/* Toggle Circle */}
|
||||
<motion.div
|
||||
className="absolute top-1 w-5 h-5 bg-white dark:bg-gray-800 rounded-full shadow-md flex items-center justify-center border border-gray-200 dark:border-gray-600"
|
||||
animate={{ x: isDark ? 28 : 0 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
>
|
||||
{/* Icon inside circle */}
|
||||
{isDark ? (
|
||||
<svg className="w-3 h-3 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3 h-3 text-gray-700" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Background Icons */}
|
||||
<div className="absolute inset-0 flex items-center justify-between px-2 pointer-events-none">
|
||||
{/* Sun Icon */}
|
||||
<svg
|
||||
className={`w-4 h-4 transition-opacity duration-300 ${isDark ? 'opacity-0' : 'opacity-100 text-yellow-500'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
{/* Moon Icon */}
|
||||
<svg
|
||||
className={`w-4 h-4 transition-opacity duration-300 ${isDark ? 'opacity-100 text-blue-400' : 'opacity-0 text-gray-700'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
</motion.button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const ThemeContext = createContext();
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const ThemeProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
// Check localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
return savedTheme;
|
||||
}
|
||||
// Check system preference
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (theme === 'dark') {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
toggleTheme,
|
||||
isDark: theme === 'dark'
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeContext;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
const userData = localStorage.getItem('user');
|
||||
|
||||
if (token && userData) {
|
||||
setIsAuthenticated(true);
|
||||
setUser(JSON.parse(userData));
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const login = (token, userData) => {
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
setIsAuthenticated(true);
|
||||
setUser(userData);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
user,
|
||||
loading,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue