commit TA

This commit is contained in:
WahyuTegarP 2026-04-02 18:57:33 +07:00
commit 0d1ce24fee
83 changed files with 16232 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -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

65
.env.example Normal file
View File

@ -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}"

11
.gitattributes vendored Normal file
View File

@ -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

24
.gitignore vendored Normal file
View File

@ -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

59
README.md Normal file
View File

@ -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).

45
README_ADMIN.md Normal file
View File

@ -0,0 +1,45 @@
# Kredensial Login Admin PawMedic
## Default Admin Credentials
**Email:** `admin@pawmedic.app`
**Password:** `admin123`
## Cara Setup Admin
1. **Jalankan migration dan seeder:**
```bash
php artisan migrate
php artisan db:seed
```
2. **Atau buat user admin manual:**
```bash
php artisan tinker
```
Kemudian jalankan:
```php
App\Models\User::create([
'name' => 'Admin PawMedic',
'email' => 'admin@pawmedic.app',
'password' => Hash::make('admin123'),
'email_verified_at' => now(),
]);
```
## Akses Login Admin
1. Buka halaman landing page
2. Scroll ke footer
3. Klik icon kunci (🔐) di pojok kanan bawah footer
4. Atau langsung akses: `/admin/login`
## Keamanan
⚠️ **PENTING:** Setelah pertama kali login, segera ubah password admin untuk keamanan!
## Mengubah Password Admin
Anda dapat mengubah password melalui:
1. Tinker: `php artisan tinker`
2. Atau buat fitur change password di dashboard admin

20
README_EXCEL.md Normal file
View File

@ -0,0 +1,20 @@
# Cara Membaca Data dari Excel DatasetTraining.xlsx
Untuk membaca data gejala dari file Excel, Anda perlu menginstall library PhpSpreadsheet:
```bash
composer require phpoffice/phpspreadsheet
```
Setelah library terinstall, controller `DiagnosisController` akan otomatis membaca data dari file `public/data/DatasetTraining.xlsx`.
Jika library belum terinstall, sistem akan menggunakan data gejala default yang sudah tersedia.
## Struktur Excel yang Diharapkan
File Excel sebaiknya memiliki struktur:
- Baris pertama: Header (akan di-skip)
- Kolom-kolom: Berisi nama gejala
- Kolom terakhir: Biasanya berisi nama penyakit/diagnosis
Controller akan membaca semua kolom kecuali kolom terakhir sebagai data gejala.

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AdminController extends Controller
{
public function login()
{
if (Auth::check()) {
return redirect()->route('admin.dashboard');
}
return view('admin.login');
}
public function authenticate(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials, $request->boolean('remember'))) {
$request->session()->regenerate();
return redirect()->intended(route('admin.dashboard'));
}
return back()->withErrors([
'email' => 'Email atau password tidak valid.',
])->onlyInput('email');
}
public function dashboard()
{
if (!Auth::check()) {
return redirect()->route('admin.login');
}
// Get statistics
$stats = $this->getStatistics();
return view('admin.dashboard', [
'stats' => $stats
]);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login');
}
private function getStatistics()
{
// Simulated statistics - bisa diganti dengan data real dari database
return [
'total_diagnosis' => 156,
'today_diagnosis' => 12,
'total_users' => 89,
'most_common_disease' => 'Feline Panleukopenia',
'recent_diagnosis' => [
['date' => '2026-01-15', 'disease' => 'Feline Panleukopenia', 'count' => 5],
['date' => '2026-01-14', 'disease' => 'Feline Calicivirus', 'count' => 3],
['date' => '2026-01-13', 'disease' => 'Scabies', 'count' => 4],
]
];
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class DiagnosisController extends Controller
{
public function prosesDiagnosis(Request $request)
{
$input = $request->input('gejala', []);
if (count($input) < 3) {
return redirect()->route('gejala')
->with('error', 'Pilih minimal 3 gejala!');
}
// 🔥 LANGSUNG PAKAI INPUT
$inputNama = $input;
// ambil feature dari Python
$response = Http::get('http://127.0.0.1:5000/gejala');
if (!$response->successful()) {
return redirect()->route('gejala')
->with('error', 'Tidak bisa mengambil data gejala dari API');
}
$featureCols = $response->json();
// bikin vector
$fiturAssoc = [];
foreach ($featureCols as $col) {
$fiturAssoc[$col] = in_array(trim($col), array_map('trim', $inputNama)) ? 1 : 0;
}
// kirim ke Flask
$response = Http::post('http://127.0.0.1:5000/predict', $fiturAssoc);
if (!$response->successful()) {
return redirect()->route('gejala')
->with('error', 'Server AI tidak merespon!');
}
$data = $response->json();
$hasil = isset($data['hasil']) ? trim($data['hasil']) : 'Tidak diketahui';
return redirect()->route('hasil-diagnosis')
->with('hasil', $hasil)
->with('gejala', $inputNama);
}}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class GejalaController extends Controller
{
public function index()
{
$response = Http::get('http://127.0.0.1:5000/gejala');
if (!$response->successful()) {
return back()->with('error', 'Tidak bisa mengambil data gejala dari API');
}
$gejala = $response->json();
return view('gejala', compact('gejala'));
}
}

11
app/Models/Gejala.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Gejala extends Model
{
protected $table = 'gejala';
public $timestamps = false;
}

48
app/Models/User.php Normal file
View File

@ -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',
];
}
}

View File

@ -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
{
//
}
}

18
artisan Normal file
View File

@ -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);

18
bootstrap/app.php Normal file
View File

@ -0,0 +1,18 @@
<?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',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

5
bootstrap/providers.php Normal file
View File

@ -0,0 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
];

86
composer.json Normal file
View File

@ -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\" \"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
}

8382
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

126
config/app.php Normal file
View File

@ -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'),
],
];

115
config/auth.php Normal file
View File

@ -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),
];

117
config/cache.php Normal file
View File

@ -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-'),
];

183
config/database.php Normal file
View File

@ -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' => '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),
],
],
];

80
config/filesystems.php Normal file
View File

@ -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' => env('APP_URL').'/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'),
],
];

132
config/logging.php Normal file
View File

@ -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'),
],
],
];

118
config/mail.php Normal file
View File

@ -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'),
],
];

129
config/queue.php Normal file
View File

@ -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',
],
];

38
config/services.php Normal file
View File

@ -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'),
],
],
];

217
config/session.php Normal file
View File

@ -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),
];

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -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,
]);
}
}

View File

@ -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');
}
};

View File

@ -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');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -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');
}
};

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// Create Admin User
User::create([
'name' => 'Admin PawMedic',
'email' => 'admin@pawmedic.app',
'password' => \Illuminate\Support\Facades\Hash::make('admin123'),
'email_verified_at' => now(),
]);
// Optional: Create test user
// User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
}
}

17
package.json Normal file
View File

@ -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"
}
}

35
phpunit.xml Normal file
View File

@ -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>

25
public/.htaccess Normal file
View File

@ -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
public/favicon.ico Normal file
View File

BIN
public/img/ucing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

20
public/index.php Normal file
View File

@ -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());

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

47
python_api/app.py Normal file
View File

@ -0,0 +1,47 @@
from flask import Flask, request, jsonify
import joblib
import json
import pandas as pd
app = Flask(__name__)
# load model
model = joblib.load("../python_artifacts/modell.joblib")
# load fitur
with open("../python_artifacts/feature_cols.json") as f:
feature_cols = json.load(f)
# endpoint gejala
@app.route('/gejala', methods=['GET'])
def get_gejala():
return jsonify(feature_cols)
# endpoint predict
@app.route('/predict', methods=['POST'])
def predict():
data = request.json
print("INPUT FROM LARAVEL:", data)
input_data = []
for col in feature_cols:
val = data.get(col, 0)
input_data.append(1 if str(val) == "1" else 0)
print("INPUT VECTOR:", input_data)
input_df = pd.DataFrame([input_data], columns=feature_cols)
hasil = model.predict(input_df)[0]
return jsonify({
"hasil": hasil
})
if __name__ == '__main__':
print("API MODEL SIAP DIGUNAKAN (DARI JUPYTER)")
app.run(debug=True)
print("FEATURE COLS:", feature_cols)
print("INPUT FROM LARAVEL:", data)
print("INPUT VECTOR:", input_data)

View File

@ -0,0 +1 @@
{"Cacingan": "Parasit", "Diare Non Spesifik": "Virus / Parasit", "Diare Spesifik": "Virus / Parasit", "Earmite": "Parasit", "FLUTD (Feline Lower Urinary Tract Diseases)": "Virus / Lingkungan", "Feline Infectious Peritonitis": "Virus", "Feline calicivirus": "Virus", "Feline herpesvirus/rhinotracheitis": "Virus", "Feline panleukopenia ": "Virus", "Jamur/Ringworm": "Parasit", "Penyakit kutu dan pinjal serta gejala alergi kulit yang menyerta": "Parasit", "Scabies": "Parasit", "URI (upper respirstory infection) bakterial": "Virus"}

View File

@ -0,0 +1 @@
["Demam Tinggi", "Muntah", "Nafsu Makan Turun", "Kelemahan", "Dehidrasi", "Pincang", "Luka pada Mulut", "Gangguan Mata", "Flu / Pilek (Gangguan Hidung)", "Sesak Napas", "Batuk", "Selaput Lendir Kuning (Jaundice)", "Perut Membesar/Buncit", "Anemia", "Diare", "Diare Berdarah", "Luka Garukan / Gatal", "Kebotakan / Rontok", "Infeksi Kulit", "Kutu / Pinjal Terlihat", "Overgrooming (Menjilat Berlebihan)", "Sulit Kencing / Sering Berjongkok Lama di Kotak Pasir", "Perut Bawah Keras", "Sakit Perut / Nyeri Abdomen", "Penurunan Berat Badan Cepat"]

View File

@ -0,0 +1 @@
["Demam Tinggi", "Muntah", "Nafsu Makan Turun", "Kelemahan", "Dehidrasi", "Pincang", "Sesak Napas", "Batuk", "Selaput Lendir Kuning (Jaundice)", "Anemia", "Diare", "Diare Berdarah", "Kutu / Pinjal Terlihat", "Overgrooming (Menjilat Berlebihan)", "Perut Bawah Keras", "Sakit Perut / Nyeri Abdomen", "Penurunan Berat Badan Cepat"]

Binary file not shown.

Binary file not shown.

11
resources/css/app.css Normal file
View File

@ -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';
}

1
resources/js/app.js Normal file
View File

@ -0,0 +1 @@
import './bootstrap';

4
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -0,0 +1,441 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Admin - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--danger: #ef4444;
--warning: #f59e0b;
--info: #3b82f6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
}
/* ===== HEADER ===== */
.admin-header{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-bottom:1px solid rgba(111,207,151,0.2);
padding:16px 0;
position:sticky;
top:0;
z-index:100;
box-shadow:0 2px 12px rgba(17,77,58,0.08);
}
.header-content{
max-width:1400px;
margin:0 auto;
padding:0 32px;
display:flex;
justify-content:space-between;
align-items:center;
}
.logo-section{
display:flex;
align-items:center;
gap:12px;
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:20px;
font-weight:700;
font-family:var(--ff-heading);
color:var(--text-dark);
}
.user-menu{
display:flex;
align-items:center;
gap:16px;
}
.user-info{
display:flex;
align-items:center;
gap:12px;
}
.avatar{
width:40px;
height:40px;
border-radius:50%;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
display:flex;
align-items:center;
justify-content:center;
color:white;
font-weight:700;
font-size:16px;
}
.logout-btn{
padding:10px 20px;
background:white;
border:2px solid var(--primary);
border-radius:10px;
color:var(--primary-dark);
text-decoration:none;
font-weight:600;
font-size:14px;
transition:all 0.3s ease;
}
.logout-btn:hover{
background:var(--primary-light);
transform:translateY(-2px);
}
/* ===== MAIN CONTENT ===== */
.container{
max-width:1400px;
margin:0 auto;
padding:40px 32px;
}
.page-title{
font-family:var(--ff-heading);
font-size:32px;
font-weight:800;
color:var(--text-dark);
margin-bottom:32px;
letter-spacing:-0.02em;
}
/* ===== STATS GRID ===== */
.stats-grid{
display:grid;
grid-template-columns:repeat(auto-fit, minmax(250px, 1fr));
gap:24px;
margin-bottom:40px;
}
.stat-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:20px;
padding:28px;
box-shadow:0 8px 24px rgba(17,77,58,0.1);
border:1px solid rgba(111,207,151,0.15);
transition:all 0.3s ease;
position:relative;
overflow:hidden;
}
.stat-card::before{
content:'';
position:absolute;
top:0;
left:0;
right:0;
height:4px;
background:linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
}
.stat-card:hover{
transform:translateY(-4px);
box-shadow:0 12px 32px rgba(17,77,58,0.15);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:16px;
}
.stat-icon{
width:56px;
height:56px;
border-radius:14px;
display:flex;
align-items:center;
justify-content:center;
font-size:28px;
background:var(--primary-light);
}
.stat-value{
font-family:var(--ff-heading);
font-size:36px;
font-weight:800;
color:var(--text-dark);
margin-bottom:4px;
}
.stat-label{
font-size:14px;
color:var(--text-muted);
font-weight:600;
text-transform:uppercase;
letter-spacing:0.5px;
}
.stat-change{
font-size:13px;
color:var(--primary-dark);
font-weight:600;
margin-top:8px;
}
/* ===== DATA TABLE ===== */
.data-section{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:24px;
padding:32px;
box-shadow:0 8px 24px rgba(17,77,58,0.1);
border:1px solid rgba(111,207,151,0.15);
margin-bottom:32px;
}
.section-title{
font-family:var(--ff-heading);
font-size:22px;
font-weight:700;
color:var(--text-dark);
margin-bottom:24px;
display:flex;
align-items:center;
gap:12px;
}
.table{
width:100%;
border-collapse:collapse;
}
.table th{
text-align:left;
padding:16px;
background:var(--primary-light);
color:var(--text-dark);
font-weight:700;
font-size:14px;
text-transform:uppercase;
letter-spacing:0.5px;
border-bottom:2px solid var(--primary);
}
.table td{
padding:16px;
border-bottom:1px solid var(--border, #e2e8f0);
color:var(--text-dark);
}
.table tr:hover{
background:var(--primary-light);
}
.badge{
padding:6px 12px;
border-radius:20px;
font-size:12px;
font-weight:600;
display:inline-block;
}
.badge-success{
background:var(--primary-light);
color:var(--primary-dark);
}
.badge-warning{
background:#fef3c7;
color:#92400e;
}
@media(max-width:768px){
.header-content{
padding:0 20px;
flex-direction:column;
gap:16px;
}
.stats-grid{
grid-template-columns:1fr;
}
.container{
padding:24px 20px;
}
.table{
font-size:14px;
}
.table th,
.table td{
padding:12px 8px;
}
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="admin-header">
<div class="header-content">
<div class="logo-section">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic Admin</div>
</div>
<div class="user-menu">
<div class="user-info">
<div class="avatar">{{ substr(Auth::user()->name ?? 'A', 0, 1) }}</div>
<div>
<div style="font-weight:600; color:var(--text-dark);">{{ Auth::user()->name ?? 'Admin' }}</div>
<div style="font-size:12px; color:var(--text-muted);">{{ Auth::user()->email ?? 'admin@pawmedic.app' }}</div>
</div>
</div>
<form method="POST" action="{{ route('admin.logout') }}" style="display:inline;">
@csrf
<button type="submit" class="logout-btn" style="border:none; background:transparent; cursor:pointer; padding:10px 20px; background:white; border:2px solid var(--primary); border-radius:10px; color:var(--primary-dark); font-weight:600; font-size:14px;">
Keluar
</button>
</form>
</div>
</div>
</div>
<!-- MAIN CONTENT -->
<div class="container">
<h1 class="page-title">Dashboard Admin</h1>
<!-- Statistics -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-value">{{ $stats['total_diagnosis'] }}</div>
<div class="stat-label">Total Diagnosis</div>
</div>
<div class="stat-icon">📊</div>
</div>
<div class="stat-change">+12 hari ini</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-value">{{ $stats['today_diagnosis'] }}</div>
<div class="stat-label">Diagnosis Hari Ini</div>
</div>
<div class="stat-icon">📈</div>
</div>
<div class="stat-change">Aktif hari ini</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-value">{{ $stats['total_users'] }}</div>
<div class="stat-label">Total Pengguna</div>
</div>
<div class="stat-icon">👥</div>
</div>
<div class="stat-change">Pengguna aktif</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-value" style="font-size:24px;">{{ $stats['most_common_disease'] }}</div>
<div class="stat-label">Penyakit Paling Umum</div>
</div>
<div class="stat-icon">🩺</div>
</div>
<div class="stat-change">Paling sering didiagnosis</div>
</div>
</div>
<!-- Recent Diagnosis -->
<div class="data-section">
<div class="section-title">
<span>📋</span>
<span>Diagnosis Terbaru</span>
</div>
<table class="table">
<thead>
<tr>
<th>Tanggal</th>
<th>Penyakit</th>
<th>Jumlah</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach($stats['recent_diagnosis'] as $diagnosis)
<tr>
<td>{{ \Carbon\Carbon::parse($diagnosis['date'])->format('d M Y') }}</td>
<td>{{ $diagnosis['disease'] }}</td>
<td>{{ $diagnosis['count'] }} kasus</td>
<td><span class="badge badge-success">Aktif</span></td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Quick Actions -->
<div class="data-section">
<div class="section-title">
<span></span>
<span>Aksi Cepat</span>
</div>
<div style="display:flex; gap:16px; flex-wrap:wrap;">
<a href="/" style="padding:12px 24px; background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color:white; text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
🏠 Lihat Website
</a>
<a href="{{ route('ulasan') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
💬 Lihat Ulasan
</a>
<a href="{{ route('faq') }}" style="padding:12px 24px; background:white; border:2px solid var(--primary); color:var(--primary-dark); text-decoration:none; border-radius:12px; font-weight:600; transition:all 0.3s ease;">
Lihat FAQ
</a>
</div>
</div>
</div>
@include('components.scroll-top')
</body>
</html>

View File

@ -0,0 +1,341 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Admin - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--danger: #ef4444;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:20px;
position:relative;
}
body::before{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
radial-gradient(circle at 20% 50%, rgba(111,207,151,0.12) 0%, transparent 60%),
radial-gradient(circle at 80% 80%, rgba(111,207,151,0.08) 0%, transparent 60%);
pointer-events:none;
z-index:0;
}
.login-container{
width:100%;
max-width:450px;
position:relative;
z-index:1;
}
.login-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:28px;
padding:48px;
box-shadow:
0 25px 80px rgba(17,77,58,0.15),
0 0 0 1px rgba(111,207,151,0.1);
border:1px solid rgba(111,207,151,0.2);
animation:fadeUp 0.8s ease;
position:relative;
overflow:hidden;
}
.login-card::before{
content:'';
position:absolute;
top:0;
left:0;
right:0;
height:5px;
background:linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
}
.logo-section{
text-align:center;
margin-bottom:40px;
}
.logo{
display:inline-flex;
align-items:center;
gap:12px;
margin-bottom:24px;
}
.logo-icon{
width:56px;
height:56px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:14px;
display:flex;
align-items:center;
justify-content:center;
font-size:28px;
box-shadow:0 8px 24px rgba(111,207,151,0.3);
}
.logo-text{
font-size:26px;
font-weight:700;
font-family:var(--ff-heading);
color:var(--text-dark);
}
.login-title{
font-family:var(--ff-heading);
font-size:28px;
font-weight:800;
color:var(--text-dark);
margin-bottom:8px;
letter-spacing:-0.02em;
}
.login-subtitle{
color:var(--text-muted);
font-size:15px;
}
.form-group{
margin-bottom:24px;
}
.form-group label{
display:block;
font-weight:600;
color:var(--text-dark);
margin-bottom:10px;
font-size:15px;
}
.form-group input{
width:100%;
padding:14px 18px;
border:2px solid var(--border, #e2e8f0);
border-radius:12px;
font-size:15px;
font-family:var(--ff-body);
transition:all 0.3s ease;
background:#fafafa;
}
.form-group input:focus{
outline:none;
border-color:var(--primary);
background:white;
box-shadow:0 0 0 4px rgba(111,207,151,0.1);
}
.remember-forgot{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:28px;
font-size:14px;
}
.remember{
display:flex;
align-items:center;
gap:8px;
}
.remember input[type="checkbox"]{
width:18px;
height:18px;
cursor:pointer;
}
.forgot-link{
color:var(--primary-dark);
text-decoration:none;
font-weight:600;
transition:color 0.2s ease;
}
.forgot-link:hover{
color:var(--primary);
}
.btn{
width:100%;
padding:16px;
border:none;
border-radius:14px;
font-weight:600;
font-size:16px;
cursor:pointer;
transition:all 0.3s ease;
font-family:var(--ff-body);
}
.btn-primary{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
}
.btn-primary::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
transition:left 0.6s;
}
.btn-primary:hover::before{
left:100%;
}
.btn-primary:hover{
transform:translateY(-2px);
box-shadow:0 8px 24px rgba(111,207,151,0.4);
}
.error-message{
background:#fee2e2;
border-left:4px solid var(--danger);
padding:12px 16px;
border-radius:8px;
color:#991b1b;
font-size:14px;
margin-bottom:24px;
display:flex;
align-items:center;
gap:8px;
}
.back-link{
display:inline-flex;
align-items:center;
gap:8px;
color:var(--text-muted);
text-decoration:none;
font-weight:600;
margin-top:24px;
transition:all 0.3s ease;
font-size:14px;
}
.back-link:hover{
color:var(--primary-dark);
transform:translateX(-4px);
}
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-card">
<div class="logo-section">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</div>
<h1 class="login-title">Admin Login</h1>
<p class="login-subtitle">Masuk ke dashboard admin</p>
</div>
@if($errors->any())
<div class="error-message">
<span>⚠️</span>
<span>{{ $errors->first() }}</span>
</div>
@endif
<form method="POST" action="{{ route('admin.authenticate') }}">
@csrf
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
value="{{ old('email') }}"
placeholder="admin@pawmedic.app"
required
autofocus
>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
placeholder="Masukkan password"
required
>
</div>
<div class="remember-forgot">
<label class="remember">
<input type="checkbox" name="remember">
<span>Ingat saya</span>
</label>
<a href="#" class="forgot-link">Lupa password?</a>
</div>
<button type="submit" class="btn btn-primary">
Masuk
</button>
</form>
<a href="/" class="back-link">
Kembali ke Beranda
</a>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,515 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Input Biodata - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
/* ===== GLOBAL ===== */
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--border: #e2e8f0;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 100%);
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
padding:20px;
}
.container{
max-width:800px;
margin:0 auto;
padding:40px 0;
}
/* ===== HEADER ===== */
.header{
text-align:center;
margin-bottom:40px;
animation:fadeDown 0.6s ease;
}
.logo-link{
display:inline-flex;
align-items:center;
gap:10px;
text-decoration:none;
color:var(--text-dark);
margin-bottom:20px;
transition:transform 0.3s ease;
}
.logo-link:hover{
transform:translateY(-2px);
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:22px;
font-weight:700;
font-family:var(--ff-heading);
}
.header h1{
font-family:var(--ff-heading);
font-size:clamp(1.8rem, 4vw, 2.4rem);
font-weight:800;
color:var(--text-dark);
margin:20px 0 12px;
letter-spacing:-0.02em;
}
.header p{
color:var(--text-muted);
font-size:16px;
margin:0;
}
/* ===== CARD FORM ===== */
.form-card{
background:white;
border-radius:24px;
padding:48px;
box-shadow:0 20px 60px rgba(17,77,58,0.1);
animation:fadeUp 0.8s ease;
border:1px solid rgba(111,207,151,0.1);
}
/* ===== FORM ===== */
.form-group{
margin-bottom:28px;
}
.form-group label{
display:block;
font-weight:600;
color:var(--text-dark);
margin-bottom:10px;
font-size:15px;
font-family:var(--ff-heading);
}
.form-group label .required{
color:#ef4444;
margin-left:4px;
}
.form-group input,
.form-group textarea,
.form-group select{
width:100%;
padding:14px 18px;
border:2px solid var(--border);
border-radius:12px;
font-size:15px;
font-family:var(--ff-body);
transition:all 0.3s ease;
background:#fafafa;
color:#333;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus{
outline:none;
border-color:var(--primary);
background:white;
box-shadow:0 0 0 4px rgba(111,207,151,0.1);
}
.form-group textarea{
resize:vertical;
min-height:100px;
font-family:var(--ff-body);
}
.form-group small{
display:block;
margin-top:6px;
color:var(--text-muted);
font-size:13px;
}
.form-row{
display:grid;
grid-template-columns:1fr 1fr;
gap:20px;
}
/* ===== BUTTON ===== */
.btn-group{
display:flex;
gap:16px;
margin-top:40px;
padding-top:32px;
border-top:1px solid var(--border);
}
.btn{
flex:1;
padding:16px 32px;
border:none;
border-radius:14px;
font-weight:600;
font-size:16px;
cursor:pointer;
transition:all 0.3s ease;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
font-family:var(--ff-body);
letter-spacing:0.3px;
}
.btn-primary{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
}
.btn-primary::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
transition:left 0.6s;
}
.btn-primary:hover::before{
left:100%;
}
.btn-primary:hover{
transform:translateY(-3px) scale(1.02);
box-shadow:0 12px 32px rgba(17,77,58,0.25);
}
.btn-secondary{
background:white;
color:var(--primary-dark);
border:2px solid var(--primary);
box-shadow:0 2px 8px rgba(17,77,58,0.1);
}
.btn-secondary:hover{
background:var(--primary-light);
transform:translateY(-2px);
box-shadow:0 8px 20px rgba(17,77,58,0.15);
}
/* ===== ANIMATIONS ===== */
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
/* ===== RESPONSIVE ===== */
@media(max-width:768px){
body{
padding:16px;
}
.container{
padding:20px 0;
}
.form-card{
padding:32px 24px;
border-radius:20px;
}
.form-row{
grid-template-columns:1fr;
gap:0;
}
.btn-group{
flex-direction:column;
}
.btn{
width:100%;
}
.header h1{
font-size:1.8rem;
}
}
@media(max-width:480px){
.form-card{
padding:24px 20px;
}
.header h1{
font-size:1.6rem;
}
.form-group{
margin-bottom:24px;
}
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER -->
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</a>
@php
$breadcrumbItems = [
['label' => 'Beranda', 'url' => '/'],
['label' => 'Biodata', 'url' => '#']
];
@endphp
@include('components.breadcrumb', ['items' => $breadcrumbItems])
<h1>Input Biodata Kucing</h1>
<p>Lengkapi informasi kucing Anda untuk memulai diagnosis</p>
</div>
<!-- FORM CARD -->
<div class="form-card">
<form action="#" method="POST" id="biodataForm">
@csrf
<!-- Nama Pemilik -->
<div class="form-group">
<label for="nama_pemilik">
Nama Pemilik <span class="required">*</span>
</label>
<input
type="text"
id="nama_pemilik"
name="nama_pemilik"
placeholder="Masukkan nama Anda"
required
autocomplete="name"
>
</div>
<!-- Nama Kucing -->
<div class="form-group">
<label for="nama_kucing">
Nama Kucing <span class="required">*</span>
</label>
<input
type="text"
id="nama_kucing"
name="nama_kucing"
placeholder="Masukkan nama kucing"
required
>
</div>
<!-- Umur Kucing -->
<div class="form-row">
<div class="form-group">
<label for="umur_kucing">
Umur Kucing <span class="required">*</span>
</label>
<input
type="number"
id="umur_kucing"
name="umur_kucing"
placeholder="Dalam bulan"
min="0"
max="240"
required
>
<small>Contoh: 12 (untuk 1 tahun)</small>
</div>
<!-- Jenis Kelamin -->
<div class="form-group">
<label for="jenis_kelamin">
Jenis Kelamin <span class="required">*</span>
</label>
<select id="jenis_kelamin" name="jenis_kelamin" required>
<option value="">Pilih jenis kelamin</option>
<option value="jantan">Jantan</option>
<option value="betina">Betina</option>
</select>
</div>
</div>
<!-- Ras Kucing -->
<div class="form-group">
<label for="ras_kucing">
Ras Kucing
</label>
<input
type="text"
id="ras_kucing"
name="ras_kucing"
placeholder="Contoh: Persia, Angora, atau Campuran"
>
<small>Opsional - Kosongkan jika tidak tahu</small>
</div>
<!-- Berat Badan -->
<div class="form-group">
<label for="berat_badan">
Berat Badan (kg) <span class="required">*</span>
</label>
<input
type="number"
id="berat_badan"
name="berat_badan"
placeholder="Contoh: 3.5"
step="0.1"
min="0.5"
max="15"
required
>
<small>Berat badan kucing dalam kilogram</small>
</div>
<!-- Alamat -->
<div class="form-group">
<label for="alamat">
Alamat
</label>
<textarea
id="alamat"
name="alamat"
placeholder="Masukkan alamat (opsional)"
rows="3"
></textarea>
<small>Opsional - Untuk keperluan dokumentasi</small>
</div>
<!-- Nomor Telepon -->
<div class="form-group">
<label for="no_telepon">
Nomor Telepon
</label>
<input
type="tel"
id="no_telepon"
name="no_telepon"
placeholder="Contoh: 081234567890"
pattern="[0-9]{10,13}"
>
<small>Opsional - Untuk keperluan dokumentasi</small>
</div>
<!-- Button Group -->
<div class="btn-group">
<a href="/" class="btn btn-secondary">
Kembali
</a>
<button type="submit" class="btn btn-primary">
<span>Lanjutkan Diagnosis</span>
<span></span>
</button>
</div>
</form>
</div>
</div>
<script>
document.getElementById('biodataForm').addEventListener('submit', function(e) {
e.preventDefault();
// Validasi sederhana
const namaPemilik = document.getElementById('nama_pemilik').value.trim();
const namaKucing = document.getElementById('nama_kucing').value.trim();
const umurKucing = document.getElementById('umur_kucing').value;
const jenisKelamin = document.getElementById('jenis_kelamin').value;
const beratBadan = document.getElementById('berat_badan').value;
if (!namaPemilik || !namaKucing || !umurKucing || !jenisKelamin || !beratBadan) {
alert('Mohon lengkapi semua field yang wajib diisi!');
return;
}
// Simpan data ke sessionStorage untuk sementara
const formData = {
nama_pemilik: namaPemilik,
nama_kucing: namaKucing,
umur_kucing: umurKucing,
jenis_kelamin: jenisKelamin,
ras_kucing: document.getElementById('ras_kucing').value.trim(),
berat_badan: beratBadan,
alamat: document.getElementById('alamat').value.trim(),
no_telepon: document.getElementById('no_telepon').value.trim()
};
sessionStorage.setItem('biodata_kucing', JSON.stringify(formData));
// Simpan data ke sessionStorage
sessionStorage.setItem('biodata_kucing', JSON.stringify(formData));
// Show success message
if (window.showToast) {
showToast('Biodata berhasil disimpan!', 'success', 'Berhasil');
}
// Redirect ke halaman pilih gejala
setTimeout(() => {
window.location.href = '{{ route("gejala") }}';
}, 500);
});
</script>
@include('components.toast')
@include('components.scroll-top')
</body>
</html>

View File

@ -0,0 +1,58 @@
<!-- Breadcrumb Navigation -->
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb-list">
@foreach($items as $index => $item)
@if($index < count($items) - 1)
<li class="breadcrumb-item">
<a href="{{ $item['url'] }}">{{ $item['label'] }}</a>
</li>
<li class="breadcrumb-separator"></li>
@else
<li class="breadcrumb-item active">{{ $item['label'] }}</li>
@endif
@endforeach
</ol>
</nav>
<style>
.breadcrumb{
margin-bottom:24px;
animation:fadeDown 0.4s ease;
}
.breadcrumb-list{
display:flex;
align-items:center;
gap:8px;
list-style:none;
padding:0;
margin:0;
flex-wrap:wrap;
}
.breadcrumb-item{
font-size:14px;
}
.breadcrumb-item a{
color:var(--text-muted, #64748b);
text-decoration:none;
transition:color 0.2s ease;
font-weight:500;
}
.breadcrumb-item a:hover{
color:var(--primary-dark, #4bb66f);
}
.breadcrumb-item.active{
color:var(--text-dark, #114d3a);
font-weight:600;
}
.breadcrumb-separator{
color:var(--text-muted, #64748b);
font-size:16px;
user-select:none;
}
</style>

View File

@ -0,0 +1,73 @@
<!-- Scroll to Top Button -->
<button id="scrollTopBtn" class="scroll-top-btn" aria-label="Scroll to top">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 15V5M10 5L5 10M10 5L15 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<style>
.scroll-top-btn{
position:fixed;
bottom:30px;
right:30px;
width:50px;
height:50px;
background:linear-gradient(135deg, var(--primary, #6fcf97) 0%, var(--primary-dark, #4bb66f) 100%);
color:white;
border:none;
border-radius:50%;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
box-shadow:0 4px 16px rgba(111,207,151,0.4);
transition:all 0.3s ease;
z-index:1000;
opacity:0;
visibility:hidden;
transform:translateY(20px);
}
.scroll-top-btn.visible{
opacity:1;
visibility:visible;
transform:translateY(0);
}
.scroll-top-btn:hover{
transform:translateY(-4px) scale(1.1);
box-shadow:0 8px 24px rgba(111,207,151,0.5);
}
.scroll-top-btn:active{
transform:translateY(-2px) scale(1.05);
}
@media(max-width:768px){
.scroll-top-btn{
bottom:20px;
right:20px;
width:44px;
height:44px;
}
}
</style>
<script>
const scrollTopBtn = document.getElementById('scrollTopBtn');
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
});
scrollTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
</script>

View File

@ -0,0 +1,164 @@
<!-- Toast Notification System -->
<div id="toastContainer" class="toast-container"></div>
<style>
.toast-container{
position:fixed;
top:20px;
right:20px;
z-index:10000;
display:flex;
flex-direction:column;
gap:12px;
pointer-events:none;
}
.toast{
background:white;
padding:16px 20px;
border-radius:12px;
box-shadow:0 8px 24px rgba(0,0,0,0.15);
display:flex;
align-items:center;
gap:12px;
min-width:300px;
max-width:400px;
pointer-events:auto;
animation:toastSlideIn 0.3s ease;
border-left:4px solid;
position:relative;
overflow:hidden;
}
.toast::before{
content:'';
position:absolute;
left:0;
top:0;
bottom:0;
width:4px;
background:currentColor;
}
.toast.success{
border-left-color:#10b981;
color:#065f46;
}
.toast.error{
border-left-color:#ef4444;
color:#991b1b;
}
.toast.info{
border-left-color:#3b82f6;
color:#1e40af;
}
.toast.warning{
border-left-color:#f59e0b;
color:#92400e;
}
.toast-icon{
font-size:20px;
flex-shrink:0;
}
.toast-content{
flex:1;
}
.toast-title{
font-weight:600;
font-size:15px;
margin-bottom:4px;
}
.toast-message{
font-size:14px;
opacity:0.8;
}
.toast-close{
background:none;
border:none;
font-size:18px;
cursor:pointer;
opacity:0.5;
transition:opacity 0.2s;
padding:0;
width:24px;
height:24px;
display:flex;
align-items:center;
justify-content:center;
}
.toast-close:hover{
opacity:1;
}
@keyframes toastSlideIn{
from{
transform:translateX(400px);
opacity:0;
}
to{
transform:translateX(0);
opacity:1;
}
}
@keyframes toastSlideOut{
from{
transform:translateX(0);
opacity:1;
}
to{
transform:translateX(400px);
opacity:0;
}
}
.toast.hiding{
animation:toastSlideOut 0.3s ease forwards;
}
</style>
<script>
function showToast(message, type = 'info', title = null) {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
toast.innerHTML = `
<div class="toast-icon">${icons[type] || icons.info}</div>
<div class="toast-content">
${title ? `<div class="toast-title">${title}</div>` : ''}
<div class="toast-message">${message}</div>
</div>
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(toast);
// Auto remove after 5 seconds
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
// Make it globally available
window.showToast = showToast;
</script>

View File

@ -0,0 +1,308 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FAQ - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
padding:20px;
position:relative;
}
body::before{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
radial-gradient(circle at 20% 50%, rgba(111,207,151,0.12) 0%, transparent 60%),
radial-gradient(circle at 80% 80%, rgba(111,207,151,0.08) 0%, transparent 60%);
pointer-events:none;
z-index:0;
}
.container{
max-width:900px;
margin:0 auto;
padding:40px 0;
position:relative;
z-index:1;
}
.header{
text-align:center;
margin-bottom:50px;
animation:fadeDown 0.6s ease;
}
.logo-link{
display:inline-flex;
align-items:center;
gap:10px;
text-decoration:none;
color:var(--text-dark);
margin-bottom:20px;
transition:transform 0.3s ease;
}
.logo-link:hover{
transform:translateY(-2px);
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:22px;
font-weight:700;
font-family:var(--ff-heading);
}
.header h1{
font-family:var(--ff-heading);
font-size:clamp(1.8rem, 4vw, 2.6rem);
font-weight:800;
color:var(--text-dark);
margin:20px 0 12px;
letter-spacing:-0.02em;
}
.header p{
color:var(--text-muted);
font-size:17px;
margin:0;
}
.faq-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:24px;
padding:40px;
box-shadow:0 20px 60px rgba(17,77,58,0.12);
border:1px solid rgba(111,207,151,0.15);
margin-bottom:24px;
animation:fadeUp 0.6s ease backwards;
}
.faq-card:nth-child(1){animation-delay:0.1s;}
.faq-card:nth-child(2){animation-delay:0.2s;}
.faq-card:nth-child(3){animation-delay:0.3s;}
.faq-card:nth-child(4){animation-delay:0.4s;}
.faq-card:nth-child(5){animation-delay:0.5s;}
.faq-card:nth-child(n+6){animation-delay:0.6s;}
.faq-question{
font-family:var(--ff-heading);
font-size:18px;
font-weight:700;
color:var(--text-dark);
margin-bottom:12px;
display:flex;
align-items:center;
gap:12px;
cursor:pointer;
user-select:none;
}
.faq-question::before{
content:'❓';
font-size:24px;
flex-shrink:0;
}
.faq-answer{
color:var(--text-muted);
line-height:1.8;
font-size:15px;
max-height:0;
overflow:hidden;
transition:max-height 0.3s ease, padding 0.3s ease;
padding:0;
}
.faq-card.active .faq-answer{
max-height:500px;
padding-top:12px;
}
.faq-card.active .faq-question::before{
content:'✅';
}
.back-btn{
margin-bottom:30px;
display:inline-flex;
align-items:center;
gap:8px;
color:var(--text-muted);
text-decoration:none;
font-weight:600;
transition:all 0.3s ease;
}
.back-btn:hover{
color:var(--primary-dark);
transform:translateX(-4px);
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@media(max-width:768px){
.faq-card{
padding:28px 24px;
}
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-btn">
Kembali ke Beranda
</a>
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</a>
<h1>Pertanyaan Umum</h1>
<p>Temukan jawaban untuk pertanyaan yang sering diajukan</p>
</div>
<div class="faq-list">
<div class="faq-card">
<div class="faq-question">Apa itu PawMedic?</div>
<div class="faq-answer">
PawMedic adalah aplikasi sistem pakar yang membantu pemilik kucing memahami gejala dan mendapatkan rekomendasi perawatan awal. Aplikasi ini menggunakan metode sistem pakar untuk menganalisis gejala yang dipilih dan memberikan diagnosis kemungkinan penyakit.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Bagaimana cara menggunakan PawMedic?</div>
<div class="faq-answer">
Cara menggunakan PawMedic sangat mudah:
<ol style="margin:12px 0; padding-left:20px;">
<li>Isi biodata kucing Anda</li>
<li>Pilih gejala yang Anda amati pada kucing</li>
<li>Sistem akan menganalisis dan memberikan hasil diagnosis</li>
<li>Baca rekomendasi perawatan yang diberikan</li>
</ol>
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah hasil diagnosis akurat?</div>
<div class="faq-answer">
Hasil diagnosis dari PawMedic adalah sebagai panduan awal berdasarkan gejala yang Anda pilih. Untuk diagnosis yang akurat dan penanganan yang tepat, sangat disarankan untuk berkonsultasi langsung dengan dokter hewan profesional. PawMedic tidak menggantikan konsultasi medis profesional.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah data saya aman?</div>
<div class="faq-answer">
Ya, data yang Anda masukkan hanya digunakan untuk keperluan diagnosis dan tidak dibagikan kepada pihak ketiga. Semua data disimpan secara lokal di browser Anda (sessionStorage) dan tidak dikirim ke server kecuali untuk keperluan analisis diagnosis.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Berapa banyak gejala yang harus dipilih?</div>
<div class="faq-answer">
Anda dapat memilih sebanyak mungkin gejala yang sesuai dengan kondisi kucing Anda. Semakin banyak gejala yang dipilih, semakin akurat diagnosis yang akan diberikan. Namun, pastikan gejala yang dipilih benar-benar Anda amati pada kucing.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Apakah aplikasi ini gratis?</div>
<div class="faq-answer">
Ya, PawMedic sepenuhnya gratis untuk digunakan. Anda dapat melakukan diagnosis tanpa batas dan mengakses semua fitur yang tersedia tanpa biaya apapun.
</div>
</div>
<div class="faq-card">
<div class="faq-question">Bagaimana jika kucing saya dalam kondisi darurat?</div>
<div class="faq-answer">
Jika kucing Anda menunjukkan tanda-tanda darurat seperti kesulitan bernapas, kejang, tidak sadar, atau luka parah, segera bawa ke dokter hewan terdekat atau klinik hewan darurat. Jangan menunggu diagnosis dari aplikasi ini.
</div>
</div>
</div>
</div>
@include('components.scroll-top')
<script>
document.querySelectorAll('.faq-question').forEach(question => {
question.addEventListener('click', function() {
const card = this.parentElement;
const isActive = card.classList.contains('active');
// Close all other cards
document.querySelectorAll('.faq-card').forEach(c => {
c.classList.remove('active');
});
// Toggle current card
if (!isActive) {
card.classList.add('active');
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,952 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pilih Gejala - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
/* ===== GLOBAL ===== */
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--border: #e2e8f0;
--success: #10b981;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
padding:20px;
position:relative;
}
body::before{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
radial-gradient(circle at 20% 50%, rgba(111,207,151,0.12) 0%, transparent 60%),
radial-gradient(circle at 80% 80%, rgba(111,207,151,0.08) 0%, transparent 60%),
radial-gradient(circle at 40% 20%, rgba(79,172,254,0.08) 0%, transparent 60%),
radial-gradient(circle at 60% 60%, rgba(139,92,246,0.05) 0%, transparent 50%);
pointer-events:none;
z-index:0;
animation:backgroundShift 20s ease infinite;
}
body::after{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%236fcf97' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
pointer-events:none;
z-index:0;
opacity:0.4;
}
@keyframes backgroundShift{
0%, 100%{transform:translate(0, 0) scale(1);}
33%{transform:translate(20px, -20px) scale(1.05);}
66%{transform:translate(-20px, 20px) scale(0.95);}
}
.container{
max-width:1100px;
margin:0 auto;
padding:40px 0;
position:relative;
z-index:1;
}
/* ===== HEADER ===== */
.header{
text-align:center;
margin-bottom:40px;
animation:fadeDown 0.6s ease;
}
.logo-link{
display:inline-flex;
align-items:center;
gap:10px;
text-decoration:none;
color:var(--text-dark);
margin-bottom:20px;
transition:transform 0.3s ease;
}
.logo-link:hover{
transform:translateY(-2px);
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:22px;
font-weight:700;
font-family:var(--ff-heading);
}
.header h1{
font-family:var(--ff-heading);
font-size:clamp(1.8rem, 4vw, 2.4rem);
font-weight:800;
color:var(--text-dark);
margin:20px 0 12px;
letter-spacing:-0.02em;
}
.header p{
color:var(--text-muted);
font-size:16px;
margin:0;
}
.progress-container{
max-width:400px;
margin:24px auto 0;
background:white;
padding:20px 24px;
border-radius:16px;
box-shadow:0 4px 20px rgba(17,77,58,0.08);
}
.progress-bar{
width:100%;
height:8px;
background:#e8f7ef;
border-radius:10px;
margin:12px 0 0;
overflow:hidden;
position:relative;
}
.progress-fill{
height:100%;
background:linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:10px;
transition:width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
width:66%;
position:relative;
overflow:hidden;
}
.progress-fill::after{
content:'';
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation:shimmer 2s infinite;
}
@keyframes shimmer{
0%{transform:translateX(-100%);}
100%{transform:translateX(100%);}
}
.progress-text{
font-size:13px;
color:var(--text-muted);
font-weight:600;
text-align:center;
margin-bottom:8px;
}
/* ===== CARD FORM ===== */
.form-card{
background:rgba(255,255,255,0.92);
backdrop-filter:blur(20px) saturate(180%);
-webkit-backdrop-filter:blur(20px) saturate(180%);
border-radius:32px;
padding:56px;
box-shadow:
0 25px 80px rgba(17,77,58,0.15),
0 0 0 1px rgba(111,207,151,0.1),
inset 0 1px 0 rgba(255,255,255,0.95),
inset 0 -1px 0 rgba(111,207,151,0.05);
animation:fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
border:1px solid rgba(111,207,151,0.2);
position:relative;
overflow:hidden;
transform-style:preserve-3d;
transition:transform 0.3s ease, box-shadow 0.3s ease;
}
.form-card:hover{
transform:translateY(-2px);
box-shadow:
0 30px 100px rgba(17,77,58,0.18),
0 0 0 1px rgba(111,207,151,0.15),
inset 0 1px 0 rgba(255,255,255,0.95);
}
.form-card::before{
content:'';
position:absolute;
top:0;
left:0;
right:0;
height:5px;
background:linear-gradient(90deg,
var(--primary) 0%,
var(--primary-dark) 25%,
#7dd9a8 50%,
var(--primary-dark) 75%,
var(--primary) 100%);
background-size:300% 100%;
animation:gradientShift 4s ease infinite;
box-shadow:0 2px 10px rgba(111,207,151,0.3);
}
.form-card::after{
content:'';
position:absolute;
top:-50%;
right:-50%;
width:200%;
height:200%;
background:radial-gradient(circle, rgba(111,207,151,0.05) 0%, transparent 70%);
animation:rotate 20s linear infinite;
pointer-events:none;
}
@keyframes gradientShift{
0%, 100%{background-position:0% 50%;}
50%{background-position:100% 50%;}
}
@keyframes rotate{
from{transform:rotate(0deg);}
to{transform:rotate(360deg);}
}
.info-box{
background:linear-gradient(135deg, var(--primary-light) 0%, #d4f4e3 100%);
border-left:4px solid var(--primary);
padding:20px 24px;
border-radius:16px;
margin-bottom:36px;
display:flex;
align-items:flex-start;
gap:16px;
box-shadow:0 4px 12px rgba(111,207,151,0.1);
position:relative;
overflow:hidden;
}
.info-box::before{
content:'';
position:absolute;
top:-50%;
right:-20%;
width:200px;
height:200px;
background:radial-gradient(circle, rgba(111,207,151,0.1) 0%, transparent 70%);
border-radius:50%;
}
.info-box .icon{
font-size:28px;
flex-shrink:0;
filter:drop-shadow(0 2px 4px rgba(17,77,58,0.1));
position:relative;
z-index:1;
}
.info-box p{
margin:0;
color:var(--text-dark);
font-size:15px;
line-height:1.7;
position:relative;
z-index:1;
font-weight:500;
}
/* ===== GEJALA GRID ===== */
.gejala-section{
margin-bottom:40px;
}
.section-title{
font-family:var(--ff-heading);
font-size:22px;
font-weight:700;
color:var(--text-dark);
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
padding-bottom:16px;
border-bottom:2px solid var(--primary-light);
}
/* ===== SEARCH ===== */
.search-container{
position:relative;
margin-bottom:24px;
}
.search-input{
width:100%;
padding:16px 50px 16px 20px;
border:2px solid var(--border);
border-radius:14px;
font-size:15px;
font-family:var(--ff-body);
background:white;
transition:all 0.3s ease;
box-shadow:0 2px 8px rgba(0,0,0,0.04);
}
.search-input:focus{
outline:none;
border-color:var(--primary);
box-shadow:0 4px 16px rgba(111,207,151,0.15);
}
.clear-search{
position:absolute;
right:16px;
top:50%;
transform:translateY(-50%);
background:var(--text-muted);
color:white;
border:none;
width:28px;
height:28px;
border-radius:50%;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
font-size:14px;
transition:all 0.3s ease;
}
.clear-search:hover{
background:var(--primary-dark);
transform:translateY(-50%) scale(1.1);
}
.gejala-grid{
display:grid;
grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
gap:18px;
margin-bottom:36px;
}
.gejala-item{
position:relative;
animation:fadeInUp 0.5s ease backwards;
}
.gejala-item:nth-child(1){animation-delay:0.05s;}
.gejala-item:nth-child(2){animation-delay:0.1s;}
.gejala-item:nth-child(3){animation-delay:0.15s;}
.gejala-item:nth-child(4){animation-delay:0.2s;}
.gejala-item:nth-child(5){animation-delay:0.25s;}
.gejala-item:nth-child(6){animation-delay:0.3s;}
.gejala-item:nth-child(7){animation-delay:0.35s;}
.gejala-item:nth-child(8){animation-delay:0.4s;}
.gejala-item:nth-child(9){animation-delay:0.45s;}
.gejala-item:nth-child(10){animation-delay:0.5s;}
.gejala-item:nth-child(n+11){animation-delay:0.55s;}
@keyframes fadeInUp{
from{
opacity:0;
transform:translateY(20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
.gejala-checkbox{
display:none;
}
.gejala-label{
display:flex;
align-items:center;
gap:14px;
padding:20px 24px;
background:linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
border:2.5px solid var(--border);
border-radius:18px;
cursor:pointer;
transition:all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
font-size:15px;
font-weight:600;
color:var(--text-dark);
user-select:none;
position:relative;
overflow:hidden;
box-shadow:
0 4px 12px rgba(0,0,0,0.06),
0 0 0 0 rgba(111,207,151,0);
transform:perspective(1000px) rotateX(0deg);
}
.gejala-label::after{
content:'';
position:absolute;
top:50%;
left:50%;
width:0;
height:0;
border-radius:50%;
background:rgba(111,207,151,0.1);
transform:translate(-50%, -50%);
transition:width 0.6s ease, height 0.6s ease;
}
.gejala-label:hover::after{
width:300px;
height:300px;
}
.gejala-label:hover{
background:linear-gradient(135deg, var(--primary-light) 0%, #ffffff 100%);
border-color:var(--primary);
transform:translateY(-6px) scale(1.03) perspective(1000px) rotateX(-2deg);
box-shadow:
0 12px 32px rgba(111,207,151,0.25),
0 0 0 4px rgba(111,207,151,0.1),
inset 0 1px 0 rgba(255,255,255,0.9);
border-width:3px;
}
.gejala-checkbox:checked + .gejala-label{
background:linear-gradient(135deg, var(--primary-light) 0%, #d4f4e3 100%);
border-color:var(--primary);
border-width:3px;
color:var(--text-dark);
box-shadow:
0 12px 36px rgba(111,207,151,0.3),
0 0 0 5px rgba(111,207,151,0.15),
inset 0 2px 4px rgba(111,207,151,0.1);
transform:translateY(-4px) scale(1.02) perspective(1000px) rotateX(-1deg);
animation:selectedPulse 2s ease infinite;
}
@keyframes selectedPulse{
0%, 100%{
box-shadow:
0 12px 36px rgba(111,207,151,0.3),
0 0 0 5px rgba(111,207,151,0.15),
inset 0 2px 4px rgba(111,207,151,0.1);
}
50%{
box-shadow:
0 12px 36px rgba(111,207,151,0.35),
0 0 0 6px rgba(111,207,151,0.2),
inset 0 2px 4px rgba(111,207,151,0.15);
}
}
.gejala-checkbox:checked + .gejala-label::before{
content:'✓';
display:flex;
align-items:center;
justify-content:center;
width:28px;
height:28px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
border-radius:50%;
font-size:16px;
font-weight:bold;
flex-shrink:0;
box-shadow:0 4px 12px rgba(111,207,151,0.3);
animation:checkmarkPop 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes checkmarkPop{
0%{
transform:scale(0);
}
50%{
transform:scale(1.2);
}
100%{
transform:scale(1);
}
}
.gejala-label::before{
content:'';
width:28px;
height:28px;
border:2.5px solid var(--border);
border-radius:50%;
flex-shrink:0;
transition:all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background:white;
position:relative;
z-index:1;
}
.selected-count{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
padding:10px 20px;
border-radius:24px;
font-size:14px;
font-weight:700;
margin-left:auto;
box-shadow:0 4px 12px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
min-width:100px;
text-align:center;
transition:all 0.3s ease;
}
.selected-count::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition:left 0.5s;
}
.selected-count:hover::before{
left:100%;
}
.selected-count.animate{
animation:pulse 0.5s ease;
}
@keyframes pulse{
0%, 100%{transform:scale(1);}
50%{transform:scale(1.1);}
}
/* ===== BUTTON ===== */
.btn-group{
display:flex;
gap:16px;
margin-top:40px;
padding-top:32px;
border-top:1px solid var(--border);
}
.btn{
flex:1;
padding:16px 32px;
border:none;
border-radius:14px;
font-weight:600;
font-size:16px;
cursor:pointer;
transition:all 0.3s ease;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
font-family:var(--ff-body);
letter-spacing:0.3px;
}
.btn-primary{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
}
.btn-primary::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
transition:left 0.6s;
}
.btn-primary:hover::before{
left:100%;
}
.btn-primary:disabled{
opacity:0.5;
cursor:not-allowed;
transform:none;
box-shadow:0 2px 8px rgba(111,207,151,0.15);
}
.btn-primary:not(:disabled):hover{
transform:translateY(-4px) scale(1.02);
}
.btn-secondary{
background:white;
color:var(--primary-dark);
border:2px solid var(--primary);
box-shadow:0 2px 8px rgba(17,77,58,0.1);
}
.btn-secondary:hover{
background:var(--primary-light);
transform:translateY(-2px);
box-shadow:0 8px 20px rgba(17,77,58,0.15);
}
/* ===== ANIMATIONS ===== */
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
/* ===== RESPONSIVE ===== */
@media(max-width:768px){
body{
padding:16px;
}
body::before{
background-image:
radial-gradient(circle at 50% 50%, rgba(111,207,151,0.08) 0%, transparent 70%);
}
.container{
padding:20px 0;
}
.form-card{
padding:32px 24px;
border-radius:24px;
}
.gejala-grid{
grid-template-columns:1fr;
gap:14px;
}
.btn-group{
flex-direction:column;
}
.btn{
width:100%;
}
.header h1{
font-size:1.8rem;
}
.progress-container{
max-width:100%;
padding:16px 20px;
}
}
@media(max-width:480px){
.form-card{
padding:24px 20px;
border-radius:20px;
}
.header h1{
font-size:1.6rem;
}
.gejala-label{
padding:16px 18px;
font-size:14px;
gap:12px;
}
.gejala-label::before,
.gejala-checkbox:checked + .gejala-label::before{
width:24px;
height:24px;
font-size:14px;
}
.info-box{
padding:16px 20px;
gap:12px;
}
.info-box .icon{
font-size:24px;
}
.section-title{
font-size:20px;
flex-wrap:wrap;
}
.selected-count{
margin-left:0;
margin-top:12px;
width:100%;
}
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER -->
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</a>
@php
$breadcrumbItems = [
['label' => 'Beranda', 'url' => '/'],
['label' => 'Biodata', 'url' => route('biodata')],
['label' => 'Pilih Gejala', 'url' => '#']
];
@endphp
@include('components.breadcrumb', ['items' => $breadcrumbItems])
<h1>Pilih Gejala Kucing</h1>
<p>Pilih gejala yang sesuai dengan kondisi kucing Anda</p>
<div class="progress-container">
<div class="progress-text">Langkah 2 dari 3</div>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
</div>
</div>
<!-- FORM CARD -->
<div class="form-card">
<form action="{{ route('diagnosis.proses') }}" method="POST">
@csrf
<!-- Info Box -->
<div class="info-box">
<div class="icon">💡</div>
<p>Pilih semua gejala yang Anda amati pada kucing Anda. Semakin banyak gejala yang dipilih, semakin akurat diagnosis yang akan diberikan.</p>
</div>
<!-- Gejala Section -->
<div class="gejala-section">
<div class="section-title">
<span>🔍 Gejala yang Ditemukan</span>
<span class="selected-count" id="selectedCount">0 dipilih</span>
</div>
<!-- Search Bar -->
<div class="search-container">
<input
type="text"
id="searchGejala"
placeholder="🔍 Cari gejala..."
class="search-input"
>
<button type="button" id="clearSearch" class="clear-search" style="display:none;"></button>
</div>
<div class="gejala-grid" id="gejalaGrid">
@foreach($gejala as $item)
<div class="gejala-item">
<input
type="checkbox"
class="gejala-checkbox"
name="gejala[]"
value="{{ $item }}"
id="gejala_{{ $loop->index }}"
>
<label for="gejala_{{ $loop->index }}" class="gejala-label">
{{ $item }}
</label>
</div>
@endforeach
</div>
</div>
<!-- Button Group -->
<div class="btn-group">
<a href="{{ route('biodata') }}" class="btn btn-secondary">
Kembali
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<span>Lanjutkan Diagnosis</span>
<span></span>
</button>
</div>
</form>
</div>
</div>
@include('components.toast')
@include('components.scroll-top')
<script>
const checkboxes = document.querySelectorAll('.gejala-checkbox');
const submitBtn = document.getElementById('submitBtn');
const selectedCount = document.getElementById('selectedCount');
const form = document.getElementById('gejalaForm');
// Update selected count
function updateSelectedCount() {
const checked = document.querySelectorAll('.gejala-checkbox:checked').length;
selectedCount.textContent = checked + ' dipilih';
// Add animation to counter
selectedCount.classList.add('animate');
setTimeout(() => selectedCount.classList.remove('animate'), 500);
// Enable/disable submit button
if (checked > 0) {
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
} else {
submitBtn.disabled = true;
submitBtn.style.opacity = '0.6';
}
}
// Add event listeners
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelectedCount);
});
// Form submission
// Search functionality
const searchInput = document.getElementById('searchGejala');
const clearSearchBtn = document.getElementById('clearSearch');
const gejalaItems = document.querySelectorAll('.gejala-item');
searchInput.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase().trim();
if (searchTerm) {
clearSearchBtn.style.display = 'flex';
} else {
clearSearchBtn.style.display = 'none';
}
gejalaItems.forEach(item => {
const label = item.querySelector('.gejala-label');
const text = label.textContent.toLowerCase();
if (text.includes(searchTerm)) {
item.style.display = 'block';
item.style.animation = 'fadeInUp 0.3s ease';
} else {
item.style.display = 'none';
}
});
// Show no results message
const visibleItems = Array.from(gejalaItems).filter(item => item.style.display !== 'none');
const grid = document.getElementById('gejalaGrid');
let noResults = document.getElementById('noResults');
if (visibleItems.length === 0 && searchTerm) {
if (!noResults) {
noResults = document.createElement('div');
noResults.id = 'noResults';
noResults.className = 'no-results';
noResults.innerHTML = '<p>😿 Tidak ada gejala yang ditemukan</p>';
grid.appendChild(noResults);
}
} else if (noResults) {
noResults.remove();
}
});
clearSearchBtn.addEventListener('click', function() {
searchInput.value = '';
clearSearchBtn.style.display = 'none';
gejalaItems.forEach(item => {
item.style.display = 'block';
});
const noResults = document.getElementById('noResults');
if (noResults) noResults.remove();
});
// Initialize count
updateSelectedCount();
</script>
<style>
.no-results{
grid-column:1/-1;
text-align:center;
padding:60px 20px;
color:var(--text-muted);
}
.no-results p{
font-size:18px;
margin:0;
}
</style>
</body>
</html>

View File

@ -0,0 +1,566 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hasil Diagnosis - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
padding:20px;
position:relative;
}
body::before{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
radial-gradient(circle at 20% 50%, rgba(111,207,151,0.12) 0%, transparent 60%),
radial-gradient(circle at 80% 80%, rgba(111,207,151,0.08) 0%, transparent 60%);
pointer-events:none;
z-index:0;
}
.container{
max-width:1000px;
margin:0 auto;
padding:40px 0;
position:relative;
z-index:1;
}
/* ===== HEADER ===== */
.header{
text-align:center;
margin-bottom:40px;
animation:fadeDown 0.6s ease;
}
.logo-link{
display:inline-flex;
align-items:center;
gap:10px;
text-decoration:none;
color:var(--text-dark);
margin-bottom:20px;
transition:transform 0.3s ease;
}
.logo-link:hover{
transform:translateY(-2px);
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:22px;
font-weight:700;
font-family:var(--ff-heading);
}
.header h1{
font-family:var(--ff-heading);
font-size:clamp(1.8rem, 4vw, 2.4rem);
font-weight:800;
color:var(--text-dark);
margin:20px 0 12px;
letter-spacing:-0.02em;
}
/* ===== RESULT CARD ===== */
.result-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:32px;
padding:48px;
box-shadow:
0 25px 80px rgba(17,77,58,0.15),
0 0 0 1px rgba(111,207,151,0.1);
border:1px solid rgba(111,207,151,0.2);
margin-bottom:32px;
animation:fadeUp 0.8s ease;
position:relative;
overflow:hidden;
}
.result-card::before{
content:'';
position:absolute;
top:0;
left:0;
right:0;
height:5px;
background:linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
}
.result-header{
text-align:center;
margin-bottom:40px;
}
.result-icon{
width:100px;
height:100px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-size:48px;
margin:0 auto 24px;
box-shadow:0 12px 40px rgba(111,207,151,0.3);
animation:iconBounce 2s ease infinite;
}
@keyframes iconBounce{
0%, 100%{transform:translateY(0) scale(1);}
50%{transform:translateY(-10px) scale(1.05);}
}
.result-title{
font-family:var(--ff-heading);
font-size:28px;
font-weight:800;
color:var(--text-dark);
margin-bottom:12px;
}
.result-subtitle{
color:var(--text-muted);
font-size:16px;
}
/* ===== DIAGNOSIS RESULT ===== */
.diagnosis-result{
background:var(--primary-light);
border-left:5px solid var(--primary);
padding:24px;
border-radius:16px;
margin-bottom:32px;
}
.diagnosis-label{
font-size:14px;
font-weight:600;
color:var(--text-muted);
text-transform:uppercase;
letter-spacing:0.5px;
margin-bottom:8px;
}
.diagnosis-name{
font-family:var(--ff-heading);
font-size:24px;
font-weight:700;
color:var(--text-dark);
margin-bottom:4px;
}
.diagnosis-category{
font-size:15px;
color:var(--primary-dark);
font-weight:600;
}
/* ===== GEJALA LIST ===== */
.gejala-list-section{
margin-bottom:32px;
}
.section-title{
font-family:var(--ff-heading);
font-size:20px;
font-weight:700;
color:var(--text-dark);
margin-bottom:20px;
display:flex;
align-items:center;
gap:10px;
}
.gejala-list{
display:flex;
flex-wrap:wrap;
gap:12px;
}
.gejala-badge{
background:white;
border:2px solid var(--primary-light);
padding:10px 18px;
border-radius:20px;
font-size:14px;
font-weight:600;
color:var(--text-dark);
display:inline-flex;
align-items:center;
gap:8px;
transition:all 0.3s ease;
}
.gejala-badge::before{
content:'✓';
width:20px;
height:20px;
background:var(--primary);
color:white;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-size:12px;
font-weight:bold;
}
.gejala-badge:hover{
border-color:var(--primary);
transform:translateY(-2px);
box-shadow:0 4px 12px rgba(111,207,151,0.2);
}
/* ===== RECOMMENDATION ===== */
.recommendation{
background:linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%);
border-left:5px solid var(--warning);
padding:24px;
border-radius:16px;
margin-bottom:32px;
}
.recommendation-title{
font-family:var(--ff-heading);
font-size:18px;
font-weight:700;
color:var(--text-dark);
margin-bottom:16px;
display:flex;
align-items:center;
gap:10px;
}
.recommendation-list{
list-style:none;
padding:0;
margin:0;
}
.recommendation-list li{
padding:12px 0;
padding-left:32px;
position:relative;
color:var(--text-dark);
line-height:1.7;
}
.recommendation-list li::before{
content:'→';
position:absolute;
left:0;
color:var(--warning);
font-weight:bold;
font-size:18px;
}
/* ===== WARNING BOX ===== */
.warning-box{
background:linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
border-left:5px solid var(--danger);
padding:24px;
border-radius:16px;
margin-bottom:32px;
}
.warning-title{
font-family:var(--ff-heading);
font-size:18px;
font-weight:700;
color:var(--danger);
margin-bottom:12px;
display:flex;
align-items:center;
gap:10px;
}
.warning-text{
color:var(--text-dark);
line-height:1.7;
}
/* ===== BUTTONS ===== */
.action-buttons{
display:flex;
gap:16px;
margin-top:40px;
flex-wrap:wrap;
}
.btn{
flex:1;
min-width:200px;
padding:16px 32px;
border:none;
border-radius:16px;
font-weight:600;
font-size:16px;
cursor:pointer;
transition:all 0.3s ease;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
font-family:var(--ff-body);
}
.btn-primary{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
}
.btn-primary:hover{
transform:translateY(-3px);
box-shadow:0 8px 24px rgba(111,207,151,0.4);
}
.btn-secondary{
background:white;
color:var(--primary-dark);
border:2px solid var(--primary);
box-shadow:0 2px 8px rgba(17,77,58,0.1);
}
.btn-secondary:hover{
background:var(--primary-light);
transform:translateY(-2px);
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@media(max-width:768px){
.result-card{
padding:32px 24px;
}
.action-buttons{
flex-direction:column;
}
.btn{
width:100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</a>
@php
$breadcrumbItems = [
['label' => 'Beranda', 'url' => '/'],
['label' => 'Biodata', 'url' => route('biodata')],
['label' => 'Gejala', 'url' => route('gejala')],
['label' => 'Hasil Diagnosis', 'url' => '#']
];
@endphp
@include('components.breadcrumb', ['items' => $breadcrumbItems])
<h1>Hasil Diagnosis</h1>
</div>
<div class="result-card">
<div class="result-header">
<div class="result-icon">🩺</div>
<div class="result-title">Diagnosis Selesai</div>
<div class="result-subtitle">Berikut adalah hasil analisis gejala kucing Anda</div>
</div>
<!-- Diagnosis Result -->
<div class="diagnosis-result">
<div class="diagnosis-label">Kemungkinan Penyakit</div>
<div class="diagnosis-name">
{{ session('hasil') ?? 'Tidak diketahui' }}
</div>
<div class="diagnosis-category">
Jenis: {{ session('jenis') ?? '-' }}
</div>
<div style="margin-top:10px; font-weight:600; color:#4bb66f;">
Confidence: {{ session('confidence') }}%
</div>
<!-- Gejala yang Dipilih -->
<div class="gejala-list-section">
<div class="section-title">
<span>🔍 Gejala yang Dipilih</span>
</div>
<div class="gejala-list">
@foreach(session('gejala', []) as $g)
<div class="gejala-badge">{{ $g }}</div>
@endforeach
</div>
</div>
<!-- Recommendation -->
<div class="recommendation">
<div class="recommendation-title">
<span>💡 Rekomendasi Perawatan</span>
</div>
<ul class="recommendation-list">
@foreach(session('pertolongan', []) as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
</div>
<div class="recommendation">
<div class="recommendation-title">
<span>🛡️ Pencegahan</span>
</div>
<ul class="recommendation-list">
@foreach(session('pencegahan', []) as $item)
<li>{{ $item }}</li>
@endforeach
</ul>
</div>
<!-- Warning -->
<div class="warning-box">
<div class="warning-title">
<span>⚠️ Peringatan Penting</span>
</div>
<div class="warning-text">
Hasil diagnosis ini hanya sebagai panduan awal. Untuk diagnosis yang akurat dan penanganan yang tepat,
sangat disarankan untuk berkonsultasi langsung dengan dokter hewan profesional.
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a href="{{ route('gejala') }}" class="btn btn-secondary">
Diagnosis Lagi
</a>
<button onclick="printDiagnosis()" class="btn btn-secondary">
🖨️ Cetak Hasil
</button>
<button onclick="shareDiagnosis()" class="btn btn-secondary">
📤 Bagikan
</button>
<a href="/" class="btn btn-primary">
🏠 Kembali ke Beranda
</a>
</div>
</div>
</div>
@include('components.scroll-top')
<script>
// Print function
function printDiagnosis() {
window.print();
}
// Share function
function shareDiagnosis() {
const diagnosis = "{{ session('hasil') }}";
const gejala = @json(session('gejala', []));
const text = `Hasil Diagnosis PawMedic:\n\nPenyakit: ${diagnosis}\nGejala: ${gejala.join(', ')}\n\nDapatkan diagnosis di: ${window.location.origin}`;
if (navigator.share) {
navigator.share({
title: 'Hasil Diagnosis PawMedic',
text: text,
url: window.location.href
});
} else {
navigator.clipboard.writeText(text);
alert('Hasil diagnosis disalin!');
}
}
</script>
<style>
@media print{
body::before,
.action-buttons,
.back-btn{
display:none;
}
.result-card{
box-shadow:none;
border:1px solid #ddd;
}
}
</style>
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,685 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>PawMedic - Sistem Pakar Kucing</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<style>
/* ===== GLOBAL ===== */
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--space:28px;
--muted: #6b7280;
}
body{
margin:0;
font-family:var(--ff-body);
background:#f8fbff;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
}
.container{
max-width:1200px;
margin:auto;
padding:36px;
}
h1,h2,h3{
margin-top:0;
font-family:var(--ff-heading);
letter-spacing:-0.01em;
color:#114d3a;
}
/* ===== NAVBAR ===== */
.navbar{
display:flex;
justify-content:space-between;
align-items:center;
padding:18px 0;
border-bottom:1px solid #eef5f3;
margin-bottom:40px;
}
/* ===== LOGO ===== */
.logo{
display:flex;
align-items:center;
gap:10px;
cursor:pointer;
animation:fadeDown 0.8s ease;
}
.logo-icon{
width:44px;
height:44px;
background:#6fcf97;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
transition:0.4s;
}
.logo-text{
font-size:20px;
font-weight:700;
transition:0.4s;
}
.logo:hover .logo-icon{
transform:rotate(-10deg) scale(1.05);
background:#4bb66f;
}
/* ===== NAV MENU ===== */
.nav-menu{
display:flex;
gap:18px;
align-items:center;
}
.nav-menu a{
text-decoration:none;
color:#555;
font-weight:600;
transition:0.2s;
}
.nav-menu a:hover{
color:#000;
}
/* ===== BUTTON ===== */
.btn{
background:linear-gradient(135deg, #6fcf97 0%, #4bb66f 100%);
color:white;
border:none;
padding:16px 32px;
border-radius:14px;
cursor:pointer;
font-weight:600;
font-size:16px;
transition:all 0.3s ease;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
position:relative;
overflow:hidden;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
letter-spacing:0.3px;
}
.btn::before{
content:'';
position:absolute;
top:0;
left:-100%;
width:100%;
height:100%;
background:linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent);
transition:left 0.6s;
}
.btn:hover::before{
left:100%;
}
.btn:hover{
transform:translateY(-4px) scale(1.02);
box-shadow:0 12px 32px rgba(17,77,58,0.25);
background:linear-gradient(135deg, #7dd9a8 0%, #5ac77f 100%);
}
.btn:active{
transform:translateY(-2px) scale(1);
}
.btn:focus{
outline:3px solid rgba(111,207,151,0.4);
outline-offset:3px;
}
.btn.secondary{
background:#ffffff;
color:#2f855a;
border:2px solid #6fcf97;
box-shadow:0 2px 10px rgba(17,77,58,0.1);
}
.btn.secondary:hover{
background:#e8f7ef;
border-color:#4bb66f;
transform:translateY(-3px) scale(1.02);
box-shadow:0 8px 20px rgba(17,77,58,0.15);
}
/* ===== HERO ===== */
.hero{
background:linear-gradient(135deg, #eaf7f0 0%, #f0fdf4 100%);
padding:25px 50px 60px 50px;
border-radius:24px;
display:flex;
gap:50px;
align-items:flex-start;
animation:fadeUp 0.8s ease;
box-shadow:0 10px 40px rgba(17,77,58,0.08);
position:relative;
overflow:hidden;
min-height:500px;
}
.hero::before{
content:'';
position:absolute;
top:-50%;
right:-10%;
width:300px;
height:300px;
background:radial-gradient(circle, rgba(111,207,151,0.1) 0%, transparent 70%);
border-radius:50%;
}
.hero::after{
content:'';
position:absolute;
bottom:-30%;
left:-5%;
width:250px;
height:250px;
background:radial-gradient(circle, rgba(111,207,151,0.08) 0%, transparent 70%);
border-radius:50%;
}
.hero > div{
flex:1;
position:relative;
z-index:1;
}
.hero-content{
max-width:640px;
display:flex;
flex-direction:column;
justify-content:flex-start;
text-align:left;
padding-top:5px;
}
.hero-content h2{
font-size:clamp(1.8rem, 3.5vw, 2.6rem);
font-weight:800;
line-height:1.3;
color:#114d3a;
margin-bottom:16px;
margin-top:0;
text-align:left;
}
.hero-content p{
font-size:17px;
line-height:1.75;
color:#4a5568;
margin-bottom:32px;
text-align:left;
max-width:580px;
}
.hero-actions{
display:flex;
gap:16px;
margin-top:12px;
align-items:center;
}
/* ===== SECTION ===== */
section{
margin-top:100px;
padding-top:10px;
}
section h2{
margin-bottom:16px;
text-align:center;
font-size:clamp(1.8rem, 3vw, 2.4rem);
font-weight:800;
letter-spacing:-0.02em;
}
section > p{
text-align:center;
color:#64748b;
font-size:17px;
margin-bottom:40px;
max-width:650px;
margin-left:auto;
margin-right:auto;
line-height:1.7;
}
/* ===== CARD ===== */
.card{
background:white;
padding:20px;
border-radius:10px;
box-shadow:0 5px 12px rgba(0,0,0,0.06);
transition:0.3s;
}
.card:hover{
transform:translateY(-6px);
}
/* ===== FEATURES ===== */
.features{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
gap:20px;
margin-top:20px;
}
.card.feature{
min-height:200px;
display:flex;
flex-direction:column;
justify-content:flex-start;
text-align:center;
padding:32px 28px;
gap:14px;
border:1px solid rgba(111,207,151,0.1);
}
.card.feature h3{
font-size:21px;
margin-bottom:10px;
color:#114d3a;
font-weight:700;
}
.card.feature p{
font-size:15px;
line-height:1.7;
color:#64748b;
margin:0;
}
.hero-image{
display:flex;
align-items:center;
justify-content:center;
flex-shrink:0;
}
.hero-image img{
width:100%;
max-width:550px;
height:auto;
display:block;
margin:0 auto;
object-fit:contain;
filter:drop-shadow(0 10px 30px rgba(17,77,58,0.15));
}
/* ===== TESTIMONIALS ===== */
#diagnosa{
background:linear-gradient(180deg, #f6fffb 0%, #ffffff 100%);
padding:32px;
border-radius:12px;
margin-top:40px;
}
#diagnosa h2{
margin-bottom:6px;
}
.testimonials{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
gap:20px;
margin-top:20px;
align-items:start;
}
.card.testimonial{
padding:20px;
border-radius:12px;
background:linear-gradient(180deg,#ffffff,#f7fffb);
box-shadow:0 8px 20px rgba(17,77,58,0.06);
transition:transform .18s ease,box-shadow .18s ease;
display:flex;
flex-direction:column;
gap:12px;
}
.card.testimonial:hover{
transform:translateY(-6px);
box-shadow:0 18px 36px rgba(17,77,58,0.12);
}
.testimonial-head{
display:flex;
align-items:center;
gap:12px;
}
.avatar{
width:48px;
height:48px;
border-radius:50%;
background:linear-gradient(135deg,#6fcf97,#2f855a);
color:#fff;
display:flex;
align-items:center;
justify-content:center;
font-weight:700;
font-size:16px;
flex-shrink:0;
}
.avatar svg{width:24px;height:24px;display:block;}
.avatar svg path, .avatar svg circle{stroke:#fff;fill:transparent}
.avatar svg circle{fill:#fff}
.meta .rating{
color:#f6b042;
font-size:14px;
margin-bottom:4px;
}
.quote{
font-size:16px;
color:#0f3f33;
position:relative;
padding-left:14px;
margin:0;
}
.quote::before{
content:'“';
font-size:40px;
position:absolute;
left:0;
top:-6px;
color:#e6f6ed;
font-weight:700;
}
.author{
font-weight:700;
color:#2f855a;
font-size:14px;
}
@media(max-width:900px){
.testimonials{
grid-template-columns:repeat(2,1fr);
}
}
@media(max-width:520px){
.testimonials{
grid-template-columns:1fr;
}
}
/* ===== FOOTER ===== */
footer{
text-align:center;
margin-top:70px;
color:#777;
padding-top:18px;
border-top:1px solid #eef5f3;
position:relative;
}
.admin-login-link{
position:absolute;
bottom:10px;
right:10px;
width:32px;
height:32px;
display:flex;
align-items:center;
justify-content:center;
text-decoration:none;
font-size:16px;
opacity:0.3;
transition:all 0.3s ease;
border-radius:8px;
background:rgba(111,207,151,0.1);
}
.admin-login-link:hover{
opacity:0.7;
background:rgba(111,207,151,0.2);
transform:scale(1.1);
}
/* ===== ANIMATIONS ===== */
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
/* ===== RESPONSIVE ===== */
@media(max-width:900px){
.hero{
flex-direction:column;
text-align:center;
justify-content:center;
padding:48px 32px;
min-height:auto;
}
.hero-content{
margin-bottom:30px;
text-align:center;
max-width:100%;
}
.hero-content h2{
font-size:28px;
text-align:center;
}
.hero-content p{
text-align:center;
max-width:100%;
}
.hero-actions{
justify-content:center;
flex-wrap:wrap;
width:100%;
}
.hero-actions .btn{
flex:1;
min-width:200px;
}
.features{
grid-template-columns:repeat(2,1fr);
}
}
@media(max-width:500px){
.features{
grid-template-columns:1fr;
}
.hero-content h2{
font-size:22px;
}
.hero-image img{
max-width:320px;
width:100%;
}
}
</style>
</head>
<body>
<div class="container">
<!-- NAVBAR -->
<div class="navbar">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</div>
<div class="nav-menu">
<a href="#fitur">Fitur</a>
<a href="#cara">Cara Kerja</a>
<a href="{{ route('ulasan') }}">Ulasan</a>
<a href="{{ route('faq') }}">FAQ</a>
<a href="{{ route('biodata') }}" class="btn" style="padding:10px 20px; font-size:14px;">Mulai Diagnosis</a>
</div>
</div>
<!-- HERO -->
<section class="hero">
<div class="hero-content">
<h2>Bantu Jaga Kesehatan Kucing Anda</h2>
<p>
PawMedic adalah aplikasi sistem pakar yang membantu pemilik kucing
memahami gejala dan mendapatkan rekomendasi perawatan awal dengan mudah dan cepat.
</p>
<div class="hero-actions">
<a href="{{ route('biodata') }}" class="btn">
<span>Mulai Diagnosis</span>
<span>🐾</span>
</a>
<button class="btn secondary" onclick="scrollToSection('fitur')">Pelajari Lebih Lanjut</button>
</div>
</div>
<div class="hero-image">
<img src="{{ asset('img/ucing.png') }}" alt="Ilustrasi Kucing">
</div>
</section>
<!-- FITUR -->
<section id="fitur">
<h2>Fitur Utama</h2>
<p>Alat yang dirancang untuk membantu pemilik kucing memahami kondisi hewan peliharaan mereka</p>
<div class="features">
<div class="card feature">
<h3>🩺 Konsultasi Cepat</h3>
<p>Memberikan gambaran awal kondisi kesehatan kucing secara cepat dan mudah</p>
</div>
<div class="card feature">
<h3>🔍 Diagnosis Gejala</h3>
<p>Menganalisis gejala menggunakan basis pengetahuan sistem pakar</p>
</div>
<div class="card feature">
<h3>🚑 Penanganan Awal</h3>
<p>Panduan langkah awal sebelum konsultasi ke dokter hewan</p>
</div>
<div class="card feature">
<h3>🐾 Tips Perawatan</h3>
<p>Menyediakan saran perawatan dasar untuk kucing sehari-hari</p>
</div>
</div>
</section>
<!-- CARA KERJA -->
<section id="cara">
<h2>Cara Kerja</h2>
<div class="features">
<div class="card feature">
<h3>Pilih Gejala</h3>
<p>Pemilik kucing memilih gejala yang sesuai dengan kondisi kucing berdasarkan pengamatan sehari-hari.</p>
</div>
<div class="card feature">
<h3>Analisis Sistem</h3>
<p>Sistem memproses data gejala yang dipilih menggunakan metode sistem pakar untuk menghasilkan diagnosis.</p>
</div>
<div class="card feature">
<h3>Hasil Diagnosa</h3>
<p>Sistem menampilkan hasil diagnosis beserta saran perawatan dan penanganan awal yang sesuai.</p>
</div>
</div>
</section>
<!-- DIAGNOSA / TESTIMONIALS -->
<section id="diagnosa">
<h2>Ulasan Pengguna</h2>
<p>Pengalaman para pemilik kucing yang telah menggunakan PawMedic.</p>
<p style="margin-top:12px;">
<a href="{{ route('ulasan') }}" style="color:var(--primary-dark); font-weight:600; text-decoration:none; border-bottom:2px solid var(--primary); padding-bottom:2px;">
Lihat Semua Ulasan
</a>
</p>
<div class="testimonials">
<div class="card testimonial">
<div class="testimonial-head">
<div class="avatar" aria-hidden="true">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
</svg>
</div>
<div class="meta">
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
<div class="author">Siti pemilik dari <em>Kiki</em></div>
</div>
</div>
<p class="quote">PawMedic memberi panduan cepat yang membantu saya mengambil tindakan tepat pada kucing saya.</p>
</div>
<div class="card testimonial">
<div class="testimonial-head">
<div class="avatar" aria-hidden="true">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
</svg>
</div>
<div class="meta">
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
<div class="author">Budi pemilik dari <em>Cleo</em></div>
</div>
</div>
<p class="quote">Aplikasinya mudah dipahami dan rekomendasinya sangat membantu sebelum pergi ke dokter hewan.</p>
</div>
<div class="card testimonial">
<div class="testimonial-head">
<div class="avatar" aria-hidden="true">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img">
<path d="M4 10c1-3 3-4 8-4s7 1 8 4c0 5-3 7-8 7s-8-2-8-7z" stroke="#fff" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="11" r="0.9" fill="#fff"/>
<circle cx="15" cy="11" r="0.9" fill="#fff"/>
</svg>
</div>
<div class="meta">
<div class="rating" aria-label="5 dari 5 bintang">★★★★★</div>
<div class="author">Lina pemilik dari <em>Oreo</em></div>
</div>
</div>
<p class="quote">Sangat berguna! Saya merasa lebih tenang mengetahui langkah awal yang harus dilakukan.</p>
</div>
</div>
</section>
<!-- FOOTER -->
<footer>
<p>© 2026 PawMedic</p>
<p>Email: support@pawmedic.app</p>
<a href="{{ route('admin.login') }}" class="admin-login-link" title="Admin Login">🔐</a>
</footer>
</div>
<script>
function scrollToSection(id){
document.getElementById(id).scrollIntoView({
behavior:'smooth'
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,342 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memproses Diagnosis - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:20px;
overflow:hidden;
}
.container{
text-align:center;
position:relative;
z-index:1;
}
.logo{
display:inline-flex;
align-items:center;
gap:12px;
margin-bottom:40px;
animation:fadeDown 0.6s ease;
}
.logo-icon{
width:60px;
height:60px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:16px;
display:flex;
align-items:center;
justify-content:center;
font-size:28px;
box-shadow:0 8px 24px rgba(111,207,151,0.3);
animation:logoFloat 3s ease-in-out infinite;
}
@keyframes logoFloat{
0%, 100%{transform:translateY(0px) rotate(0deg);}
50%{transform:translateY(-10px) rotate(5deg);}
}
.logo-text{
font-size:28px;
font-weight:700;
font-family:var(--ff-heading);
color:var(--text-dark);
}
.loading-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:32px;
padding:60px 50px;
box-shadow:
0 25px 80px rgba(17,77,58,0.15),
0 0 0 1px rgba(111,207,151,0.1);
border:1px solid rgba(111,207,151,0.2);
max-width:500px;
margin:0 auto;
animation:fadeUp 0.8s ease;
}
h1{
font-family:var(--ff-heading);
font-size:32px;
font-weight:800;
color:var(--text-dark);
margin-bottom:16px;
letter-spacing:-0.02em;
}
p{
color:#64748b;
font-size:17px;
margin-bottom:40px;
}
.loader{
width:80px;
height:80px;
margin:0 auto 30px;
position:relative;
}
.loader-circle{
width:100%;
height:100%;
border:6px solid var(--primary-light);
border-top-color:var(--primary);
border-right-color:var(--primary-dark);
border-radius:50%;
animation:spin 1s linear infinite;
position:relative;
}
.loader-circle::before{
content:'';
position:absolute;
top:50%;
left:50%;
width:50%;
height:50%;
border:4px solid transparent;
border-top-color:var(--primary-dark);
border-radius:50%;
transform:translate(-50%, -50%);
animation:spin 0.5s linear infinite reverse;
}
@keyframes spin{
to{transform:rotate(360deg);}
}
.loading-dots{
display:flex;
gap:8px;
justify-content:center;
margin-top:20px;
}
.dot{
width:12px;
height:12px;
background:var(--primary);
border-radius:50%;
animation:dotBounce 1.4s ease-in-out infinite;
}
.dot:nth-child(1){animation-delay:0s;}
.dot:nth-child(2){animation-delay:0.2s;}
.dot:nth-child(3){animation-delay:0.4s;}
@keyframes dotBounce{
0%, 80%, 100%{transform:translateY(0) scale(1); opacity:0.7;}
40%{transform:translateY(-20px) scale(1.2); opacity:1;}
}
.status-text{
font-size:15px;
color:var(--text-dark);
font-weight:600;
margin-top:20px;
animation:pulse 2s ease infinite;
}
@keyframes pulse{
0%, 100%{opacity:1;}
50%{opacity:0.6;}
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
.particles{
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
pointer-events:none;
z-index:0;
}
.particle{
position:absolute;
width:4px;
height:4px;
background:var(--primary);
border-radius:50%;
opacity:0.3;
animation:float 15s infinite;
}
.particle:nth-child(1){left:10%; animation-delay:0s; animation-duration:12s;}
.particle:nth-child(2){left:20%; animation-delay:2s; animation-duration:15s;}
.particle:nth-child(3){left:30%; animation-delay:4s; animation-duration:18s;}
.particle:nth-child(4){left:40%; animation-delay:1s; animation-duration:14s;}
.particle:nth-child(5){left:50%; animation-delay:3s; animation-duration:16s;}
.particle:nth-child(6){left:60%; animation-delay:5s; animation-duration:13s;}
.particle:nth-child(7){left:70%; animation-delay:2.5s; animation-duration:17s;}
.particle:nth-child(8){left:80%; animation-delay:4.5s; animation-duration:15s;}
.particle:nth-child(9){left:90%; animation-delay:1.5s; animation-duration:19s;}
@keyframes float{
0%{
transform:translateY(100vh) translateX(0);
opacity:0;
}
10%{
opacity:0.3;
}
90%{
opacity:0.3;
}
100%{
transform:translateY(-100px) translateX(50px);
opacity:0;
}
}
</style>
</head>
<body>
<div class="particles">
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
<div class="particle"></div>
</div>
<div class="container">
<div class="logo">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</div>
<div class="loading-card">
<h1>Memproses Diagnosis</h1>
<p>Sedang menganalisis gejala yang Anda pilih...</p>
<div class="loader">
<div class="loader-circle"></div>
</div>
<div class="loading-dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="status-text" id="statusText">Menganalisis data gejala...</div>
</div>
</div>
<script>
const statusMessages = [
'Menganalisis data gejala...',
'Memproses dengan sistem pakar...',
'Menghitung probabilitas penyakit...',
'Menyusun rekomendasi perawatan...',
'Hampir selesai...'
];
let currentStatus = 0;
const statusText = document.getElementById('statusText');
// Update status message
setInterval(() => {
currentStatus = (currentStatus + 1) % statusMessages.length;
statusText.textContent = statusMessages[currentStatus];
}, 2000);
// Get gejala from URL
const urlParams = new URLSearchParams(window.location.search);
const gejalaParam = urlParams.get('gejala');
// Simulate processing and redirect
setTimeout(() => {
if (gejalaParam) {
// Submit form to process diagnosis
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ route("diagnosis.proses") }}';
const csrf = document.createElement('input');
csrf.type = 'hidden';
csrf.name = '_token';
csrf.value = '{{ csrf_token() }}';
form.appendChild(csrf);
const gejalaInput = document.createElement('input');
gejalaInput.type = 'hidden';
gejalaInput.name = 'gejala';
gejalaInput.value = gejalaParam;
form.appendChild(gejalaInput);
document.body.appendChild(form);
form.submit();
} else {
// Fallback: redirect to hasil
window.location.href = '/hasil-diagnosis';
}
}, 3000);
</script>
</body>
</html>

View File

@ -0,0 +1,740 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ulasan Pengguna - PawMedic</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--ff-heading: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--ff-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
--primary: #6fcf97;
--primary-dark: #4bb66f;
--primary-light: #e8f7ef;
--text-dark: #114d3a;
--text-muted: #64748b;
--warning: #f59e0b;
--border: #e2e8f0;
}
* {
box-sizing: border-box;
}
body{
margin:0;
font-family:var(--ff-body);
background:linear-gradient(135deg, #f0fdf4 0%, #eaf7f0 50%, #f0f9ff 100%);
background-attachment:fixed;
color:#333;
-webkit-font-smoothing:antialiased;
line-height:1.6;
min-height:100vh;
padding:20px;
position:relative;
}
body::before{
content:'';
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
background-image:
radial-gradient(circle at 20% 50%, rgba(111,207,151,0.12) 0%, transparent 60%),
radial-gradient(circle at 80% 80%, rgba(111,207,151,0.08) 0%, transparent 60%);
pointer-events:none;
z-index:0;
}
.container{
max-width:1200px;
margin:0 auto;
padding:40px 0;
position:relative;
z-index:1;
}
/* ===== HEADER ===== */
.header{
text-align:center;
margin-bottom:50px;
animation:fadeDown 0.6s ease;
}
.logo-link{
display:inline-flex;
align-items:center;
gap:10px;
text-decoration:none;
color:var(--text-dark);
margin-bottom:20px;
transition:transform 0.3s ease;
}
.logo-link:hover{
transform:translateY(-2px);
}
.logo-icon{
width:44px;
height:44px;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:22px;
}
.logo-text{
font-size:22px;
font-weight:700;
font-family:var(--ff-heading);
}
.header h1{
font-family:var(--ff-heading);
font-size:clamp(1.8rem, 4vw, 2.6rem);
font-weight:800;
color:var(--text-dark);
margin:20px 0 12px;
letter-spacing:-0.02em;
}
.header p{
color:var(--text-muted);
font-size:17px;
margin:0;
}
/* ===== STATS CARD ===== */
.stats-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:24px;
padding:32px;
box-shadow:0 20px 60px rgba(17,77,58,0.12);
border:1px solid rgba(111,207,151,0.15);
margin-bottom:40px;
display:grid;
grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));
gap:24px;
animation:fadeUp 0.8s ease;
}
.stat-item{
text-align:center;
}
.stat-value{
font-family:var(--ff-heading);
font-size:36px;
font-weight:800;
color:var(--primary-dark);
margin-bottom:8px;
}
.stat-label{
font-size:14px;
color:var(--text-muted);
font-weight:600;
text-transform:uppercase;
letter-spacing:0.5px;
}
/* ===== FORM CARD ===== */
.form-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:28px;
padding:40px;
box-shadow:0 20px 60px rgba(17,77,58,0.12);
border:1px solid rgba(111,207,151,0.15);
margin-bottom:40px;
animation:fadeUp 0.8s ease 0.2s backwards;
}
.form-title{
font-family:var(--ff-heading);
font-size:24px;
font-weight:700;
color:var(--text-dark);
margin-bottom:24px;
display:flex;
align-items:center;
gap:12px;
}
.form-group{
margin-bottom:24px;
}
.form-group label{
display:block;
font-weight:600;
color:var(--text-dark);
margin-bottom:10px;
font-size:15px;
}
.form-group input,
.form-group textarea{
width:100%;
padding:14px 18px;
border:2px solid var(--border);
border-radius:12px;
font-size:15px;
font-family:var(--ff-body);
transition:all 0.3s ease;
background:#fafafa;
}
.form-group input:focus,
.form-group textarea:focus{
outline:none;
border-color:var(--primary);
background:white;
box-shadow:0 0 0 4px rgba(111,207,151,0.1);
}
.form-group textarea{
resize:vertical;
min-height:120px;
}
.rating-input{
display:flex;
gap:8px;
align-items:center;
flex-wrap:wrap;
}
.star{
font-size:32px;
color:#ddd;
cursor:pointer;
transition:all 0.2s ease;
user-select:none;
}
.star:hover,
.star.active{
color:var(--warning);
transform:scale(1.1);
}
/* ===== REVIEWS GRID ===== */
.reviews-section{
margin-bottom:40px;
}
.section-header{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:24px;
flex-wrap:wrap;
gap:16px;
}
.section-title{
font-family:var(--ff-heading);
font-size:24px;
font-weight:700;
color:var(--text-dark);
}
.filter-buttons{
display:flex;
gap:8px;
flex-wrap:wrap;
}
.filter-btn{
padding:8px 16px;
border:2px solid var(--border);
background:white;
border-radius:20px;
font-size:14px;
font-weight:600;
color:var(--text-muted);
cursor:pointer;
transition:all 0.3s ease;
}
.filter-btn:hover,
.filter-btn.active{
border-color:var(--primary);
background:var(--primary-light);
color:var(--primary-dark);
}
.reviews-grid{
display:grid;
grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));
gap:24px;
}
.review-card{
background:rgba(255,255,255,0.95);
backdrop-filter:blur(20px);
border-radius:20px;
padding:24px;
box-shadow:0 8px 24px rgba(17,77,58,0.08);
border:1px solid rgba(111,207,151,0.1);
transition:all 0.3s ease;
animation:fadeUp 0.6s ease backwards;
}
.review-card:nth-child(1){animation-delay:0.1s;}
.review-card:nth-child(2){animation-delay:0.2s;}
.review-card:nth-child(3){animation-delay:0.3s;}
.review-card:nth-child(4){animation-delay:0.4s;}
.review-card:nth-child(5){animation-delay:0.5s;}
.review-card:nth-child(n+6){animation-delay:0.6s;}
.review-card:hover{
transform:translateY(-4px);
box-shadow:0 12px 32px rgba(17,77,58,0.15);
border-color:var(--primary);
}
.review-header{
display:flex;
align-items:flex-start;
gap:16px;
margin-bottom:16px;
}
.avatar{
width:56px;
height:56px;
border-radius:50%;
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
display:flex;
align-items:center;
justify-content:center;
font-size:24px;
flex-shrink:0;
box-shadow:0 4px 12px rgba(111,207,151,0.3);
}
.review-info{
flex:1;
}
.review-name{
font-weight:700;
color:var(--text-dark);
font-size:16px;
margin-bottom:4px;
}
.review-cat{
font-size:14px;
color:var(--text-muted);
font-style:italic;
}
.review-rating{
color:var(--warning);
font-size:16px;
margin-top:8px;
}
.review-text{
color:var(--text-dark);
line-height:1.7;
font-size:15px;
margin:0;
position:relative;
padding-left:20px;
}
.review-text::before{
content:'"';
position:absolute;
left:0;
top:-8px;
font-size:48px;
color:var(--primary-light);
font-weight:700;
line-height:1;
}
.review-date{
font-size:13px;
color:var(--text-muted);
margin-top:16px;
padding-top:16px;
border-top:1px solid var(--border);
}
/* ===== BUTTONS ===== */
.btn{
padding:14px 28px;
border:none;
border-radius:14px;
font-weight:600;
font-size:16px;
cursor:pointer;
transition:all 0.3s ease;
text-decoration:none;
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
font-family:var(--ff-body);
}
.btn-primary{
background:linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color:white;
box-shadow:0 4px 16px rgba(111,207,151,0.3);
}
.btn-primary:hover{
transform:translateY(-3px);
box-shadow:0 8px 24px rgba(111,207,151,0.4);
}
.btn-secondary{
background:white;
color:var(--primary-dark);
border:2px solid var(--primary);
}
.btn-secondary:hover{
background:var(--primary-light);
transform:translateY(-2px);
}
.back-btn{
margin-bottom:30px;
display:inline-flex;
align-items:center;
gap:8px;
color:var(--text-muted);
text-decoration:none;
font-weight:600;
transition:all 0.3s ease;
}
.back-btn:hover{
color:var(--primary-dark);
transform:translateX(-4px);
}
@keyframes fadeDown{
from{
opacity:0;
transform:translateY(-20px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@keyframes fadeUp{
from{
opacity:0;
transform:translateY(30px);
}
to{
opacity:1;
transform:translateY(0);
}
}
@media(max-width:768px){
.stats-card{
grid-template-columns:repeat(2, 1fr);
}
.reviews-grid{
grid-template-columns:1fr;
}
.form-card{
padding:28px 24px;
}
.section-header{
flex-direction:column;
align-items:flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-btn">
Kembali ke Beranda
</a>
<div class="header">
<a href="/" class="logo-link">
<div class="logo-icon">🐾</div>
<div class="logo-text">PawMedic</div>
</a>
<h1>Ulasan Pengguna</h1>
<p>Bagikan pengalaman Anda menggunakan PawMedic</p>
</div>
<!-- Stats -->
<div class="stats-card">
<div class="stat-item">
<div class="stat-value" id="totalReviews">12</div>
<div class="stat-label">Total Ulasan</div>
</div>
<div class="stat-item">
<div class="stat-value" id="avgRating">4.8</div>
<div class="stat-label">Rating Rata-rata</div>
</div>
<div class="stat-item">
<div class="stat-value" id="fiveStars">95%</div>
<div class="stat-label">5 Bintang</div>
</div>
</div>
<!-- Form Submit Ulasan -->
<div class="form-card">
<div class="form-title">
<span>✍️</span>
<span>Tulis Ulasan Anda</span>
</div>
<form id="reviewForm">
<div class="form-group">
<label>Nama Anda</label>
<input type="text" id="reviewName" placeholder="Masukkan nama Anda" required>
</div>
<div class="form-group">
<label>Nama Kucing</label>
<input type="text" id="reviewCat" placeholder="Nama kucing Anda (opsional)">
</div>
<div class="form-group">
<label>Rating</label>
<div class="rating-input">
<span class="star" data-rating="1"></span>
<span class="star" data-rating="2"></span>
<span class="star" data-rating="3"></span>
<span class="star" data-rating="4"></span>
<span class="star" data-rating="5"></span>
<input type="hidden" id="ratingValue" value="0" required>
</div>
</div>
<div class="form-group">
<label>Ulasan</label>
<textarea id="reviewText" placeholder="Bagikan pengalaman Anda menggunakan PawMedic..." required></textarea>
</div>
<button type="submit" class="btn btn-primary">
<span>Kirim Ulasan</span>
<span></span>
</button>
</form>
</div>
<!-- Reviews List -->
<div class="reviews-section">
<div class="section-header">
<div class="section-title">Ulasan Pengguna</div>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">Semua</button>
<button class="filter-btn" data-filter="5">5 Bintang</button>
<button class="filter-btn" data-filter="4">4 Bintang</button>
<button class="filter-btn" data-filter="3">3 Bintang</button>
</div>
</div>
<div class="reviews-grid" id="reviewsGrid">
<!-- Reviews will be populated by JavaScript -->
</div>
</div>
</div>
@include('components.toast')
@include('components.scroll-top')
<script>
// Sample reviews data
const reviews = [
{
id: 1,
name: 'Siti',
cat: 'Kiki',
rating: 5,
text: 'PawMedic memberi panduan cepat yang membantu saya mengambil tindakan tepat pada kucing saya. Sangat membantu!',
date: '2 hari yang lalu'
},
{
id: 2,
name: 'Budi',
cat: 'Cleo',
rating: 5,
text: 'Aplikasinya mudah dipahami dan rekomendasinya sangat membantu sebelum pergi ke dokter hewan.',
date: '5 hari yang lalu'
},
{
id: 3,
name: 'Lina',
cat: 'Oreo',
rating: 5,
text: 'Sangat berguna! Saya merasa lebih tenang mengetahui langkah awal yang harus dilakukan.',
date: '1 minggu yang lalu'
},
{
id: 4,
name: 'Ahmad',
cat: 'Milo',
rating: 5,
text: 'Sistem diagnosisnya akurat dan mudah digunakan. Sangat membantu untuk pemilik kucing pemula seperti saya.',
date: '2 minggu yang lalu'
},
{
id: 5,
name: 'Dewi',
cat: 'Luna',
rating: 4,
text: 'Bagus sekali aplikasinya. Interface-nya user-friendly dan informasinya lengkap.',
date: '3 minggu yang lalu'
},
{
id: 6,
name: 'Rudi',
cat: 'Max',
rating: 5,
text: 'PawMedic membantu saya memahami kondisi kucing dengan lebih baik. Terima kasih!',
date: '1 bulan yang lalu'
}
];
let currentFilter = 'all';
// Rating stars
const stars = document.querySelectorAll('.star');
const ratingInput = document.getElementById('ratingValue');
stars.forEach((star, index) => {
star.addEventListener('click', () => {
const rating = index + 1;
ratingInput.value = rating;
updateStars(rating);
});
star.addEventListener('mouseenter', () => {
updateStars(index + 1);
});
});
document.querySelector('.rating-input').addEventListener('mouseleave', () => {
const currentRating = parseInt(ratingInput.value) || 0;
updateStars(currentRating);
});
function updateStars(rating) {
stars.forEach((star, index) => {
if (index < rating) {
star.classList.add('active');
} else {
star.classList.remove('active');
}
});
}
// Display reviews
function displayReviews(filter = 'all') {
const grid = document.getElementById('reviewsGrid');
const filteredReviews = filter === 'all'
? reviews
: reviews.filter(r => r.rating === parseInt(filter));
grid.innerHTML = filteredReviews.map(review => `
<div class="review-card">
<div class="review-header">
<div class="avatar">${review.name.charAt(0)}</div>
<div class="review-info">
<div class="review-name">${review.name}</div>
<div class="review-cat">pemilik dari <em>${review.cat}</em></div>
<div class="review-rating">${'★'.repeat(review.rating)}${'☆'.repeat(5 - review.rating)}</div>
</div>
</div>
<p class="review-text">${review.text}</p>
<div class="review-date">${review.date}</div>
</div>
`).join('');
}
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
displayReviews(currentFilter);
});
});
// Form submission
document.getElementById('reviewForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('reviewName').value;
const cat = document.getElementById('reviewCat').value;
const rating = parseInt(ratingInput.value);
const text = document.getElementById('reviewText').value;
if (!rating || rating === 0) {
alert('Mohon berikan rating!');
return;
}
// Add new review
const newReview = {
id: reviews.length + 1,
name: name,
cat: cat || 'Kucing',
rating: rating,
text: text,
date: 'Baru saja'
};
reviews.unshift(newReview);
displayReviews(currentFilter);
updateStats();
// Reset form
this.reset();
ratingInput.value = 0;
updateStars(0);
// Show toast
if (window.showToast) {
showToast('Terima kasih atas ulasan Anda!', 'success', 'Ulasan Terkirim');
} else {
alert('Terima kasih atas ulasan Anda! 🐾');
}
});
// Update stats
function updateStats() {
const total = reviews.length;
const avg = (reviews.reduce((sum, r) => sum + r.rating, 0) / total).toFixed(1);
const fiveStars = Math.round((reviews.filter(r => r.rating === 5).length / total) * 100);
document.getElementById('totalReviews').textContent = total;
document.getElementById('avgRating').textContent = avg;
document.getElementById('fiveStars').textContent = fiveStars + '%';
}
// Initialize
displayReviews();
updateStats();
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

8
routes/console.php Normal file
View File

@ -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');

40
routes/web.php Normal file
View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\DiagnosisController;
use App\Http\Controllers\AdminController;
use App\Http\Controllers\GejalaController;
Route::get('/gejala', [GejalaController::class, 'index'])->name('gejala');
Route::get('/', function () {
return view('landing');
});
Route::get('/biodata', function () {
return view('biodata');
})->name('biodata');
Route::get('/loading', function () {
return view('loading');
})->name('loading');
Route::post('/diagnosis/proses', [DiagnosisController::class, 'prosesDiagnosis'])->name('diagnosis.proses');
Route::get('/hasil-diagnosis', function () {
return view('hasil-diagnosis');
})->name('hasil-diagnosis');
Route::get('/ulasan', function () {
return view('ulasan');
})->name('ulasan');
Route::get('/faq', function () {
return view('faq');
})->name('faq');
// Admin Routes
Route::get('/admin/login', [AdminController::class, 'login'])->name('admin.login');
Route::post('/admin/login', [AdminController::class, 'authenticate'])->name('admin.authenticate');
Route::post('/admin/logout', [AdminController::class, 'logout'])->name('admin.logout');
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])->name('admin.dashboard')->middleware('auth');

4
storage/app/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore

2
storage/app/private/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage/app/public/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

9
storage/framework/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json

3
storage/framework/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!data/
!.gitignore

View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage/framework/sessions/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage/framework/testing/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage/framework/views/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage/logs/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -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);
}
}

10
tests/TestCase.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View File

@ -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);
}
}

18
vite.config.js Normal file
View File

@ -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/**'],
},
},
});