From bd2aa6e9b6db1f13e8461e49996807c31876a23f Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Mon, 19 Jun 2023 01:27:41 +0700 Subject: [PATCH] home page refactor and customer favorite --- .env.example | 3 + README.md | 12 +- .../Controllers/Customer/AuthController.php | 9 +- .../Controllers/Customer/HomeController.php | 73 ++++++++- .../HandleInertiaCustomerRequests.php | 2 +- app/Models/Customer.php | 10 +- app/Models/CustomerLocationFavorite.php | 8 +- app/Models/LocationProfile.php | 2 +- app/Models/Voucher.php | 53 ++++++- ...eate_customer_location_favorites_table.php | 9 +- resources/js/Components/Modal.jsx | 38 +++-- .../js/Customer/Components/FormLocation.jsx | 59 ++++--- resources/js/Customer/Components/Modal.jsx | 15 ++ resources/js/Customer/Index/Index.jsx | 119 +++----------- .../Index/IndexPartials/AllVoucher.jsx | 148 ++++++++++++++++++ .../Index/IndexPartials/FavoriteVoucher.jsx | 108 +++++++++++++ .../Index/{ => Partials}/BalanceBanner.jsx | 0 .../Customer/Index/Partials/LocationModal.jsx | 98 ++++++++++++ .../Index/{ => Partials}/UserBanner.jsx | 0 .../Index/{ => Partials}/VoucherCard.jsx | 8 +- resources/js/Customer/Index/utils.js | 19 +++ resources/js/Customer/Profile/Index.jsx | 2 +- resources/js/Layouts/CustomerLayout.jsx | 2 +- resources/js/hooks.js | 50 +++--- routes/web.php | 4 + rsync.sh | 4 + 26 files changed, 664 insertions(+), 191 deletions(-) create mode 100644 resources/js/Customer/Components/Modal.jsx create mode 100644 resources/js/Customer/Index/IndexPartials/AllVoucher.jsx create mode 100644 resources/js/Customer/Index/IndexPartials/FavoriteVoucher.jsx rename resources/js/Customer/Index/{ => Partials}/BalanceBanner.jsx (100%) create mode 100644 resources/js/Customer/Index/Partials/LocationModal.jsx rename resources/js/Customer/Index/{ => Partials}/UserBanner.jsx (100%) rename resources/js/Customer/Index/{ => Partials}/VoucherCard.jsx (88%) create mode 100644 resources/js/Customer/Index/utils.js create mode 100755 rsync.sh diff --git a/.env.example b/.env.example index ae84bd3..bd251a7 100644 --- a/.env.example +++ b/.env.example @@ -57,6 +57,9 @@ VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + HTTPS_AWARE=false DS_APP_HOST=10.25.10.1 diff --git a/README.md b/README.md index 48398a1..652dfe5 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,20 @@ password : password npm run build ``` -## Rsync +## Other + +### v1 ```bash rsync -arP -e 'ssh -p 224' --exclude=node_modules --exclude=.git --exclude=.env --exclude=storage --exclude=public/hot . arm@ajikamaludin.id:/home/arm/projects/voucher rsync -arP -e 'ssh -p 224' --exclude=node_modules --exclude=database/database.sqlite --exclude=.git --exclude=.env --exclude=storage --exclude=public/hot . arm@ajikamaludin.id:/home/arm/projects/voucher ``` + +### v2 + +```bash +rsync -arP -e 'ssh -p 225' --exclude=node_modules --exclude=database/database.sqlite --exclude=.git --exclude=.env --exclude=public/hot . arm@ajikamaludin.id:/home/arm/projects/www/voucher + +ssh -p 225 arm@ajikamaludin.id -C docker exec php82 php /var/www/voucher/artisan migrate:refresh --seed +``` diff --git a/app/Http/Controllers/Customer/AuthController.php b/app/Http/Controllers/Customer/AuthController.php index a2a16a3..71c3fc0 100644 --- a/app/Http/Controllers/Customer/AuthController.php +++ b/app/Http/Controllers/Customer/AuthController.php @@ -62,6 +62,7 @@ class AuthController extends Controller ->setConfig($this->config) ->user(); } catch (\Exception $e) { + info('auth google error', ['exception' => $e]); return redirect()->route('customer.login') ->with('message', ['type' => 'error', 'message' => 'Google authentication fail, please try again']); } @@ -73,11 +74,13 @@ class AuthController extends Controller 'fullname' => $user->name, 'name' => $user->nickname, 'email' => $user->email, - 'username' => Str::slug($user->name.'_'.Str::random(5), '_'), + 'username' => Str::slug($user->name . '_' . Str::random(5), '_'), 'google_id' => $user->id, 'google_oauth_response' => json_encode($user), ]); DB::commit(); + } else { + $customer->update(['google_oauth_response' => json_encode($user)]); } Auth::guard('customer')->loginUsingId($customer->id); @@ -98,7 +101,7 @@ class AuthController extends Controller 'fullname' => 'required|string', 'name' => 'required|string', 'address' => 'required|string', - 'phone' => 'required|numeric|regex:/^([0-9\s\-\+\(\)]*)$/|min:9|max:16', + 'phone' => 'required|regex:/^([0-9\s\-\+\(\)]*)$/|min:9|max:16', 'username' => 'required|string|min:5|alpha_dash|unique:customers,username', 'password' => 'required|string|min:8|confirmed', 'referral_code' => 'nullable|exists:customers,referral_code', @@ -126,7 +129,7 @@ class AuthController extends Controller $bonuspoin = Setting::getByKey('AFFILATE_poin_AMOUNT'); $poin = $refferal->poins()->create([ 'debit' => $bonuspoin, - 'description' => 'Bonus Refferal #'.Str::random(5), + 'description' => 'Bonus Refferal #' . Str::random(5), ]); $poin->update_customer_balance(); diff --git a/app/Http/Controllers/Customer/HomeController.php b/app/Http/Controllers/Customer/HomeController.php index d3d9cde..c06ae18 100644 --- a/app/Http/Controllers/Customer/HomeController.php +++ b/app/Http/Controllers/Customer/HomeController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers\Customer; use App\Http\Controllers\Controller; use App\Models\Banner; +use App\Models\Customer; +use App\Models\CustomerLocationFavorite; use App\Models\Info; use App\Models\Location; use App\Models\Notification; @@ -12,25 +14,75 @@ use Illuminate\Http\Request; class HomeController extends Controller { + const LIMIT = 2; + public function index(Request $request) { + if ($request->direct != '') { + $customer = Customer::find(auth()->id()); + if ($request->location_ids == '' && $customer->locationFavorites()->count() > 0) { + return redirect()->route('customer.home.favorite'); + } + } + $infos = Info::where('is_publish', 1)->orderBy('updated_at', 'desc')->get(); $banners = Banner::orderBy('updated_at', 'desc')->get(); - $locations = Location::orderBy('updated_at', 'desc')->get(); - $vouchers = Voucher::with(['location']) + $locations = Location::orderBy('name', 'asc')->get(); + + $slocations = []; + + $vouchers = Voucher::with(['locationProfile.location']) ->where('is_sold', Voucher::UNSOLD) - ->orderBy('updated_at', 'desc'); + ->orderBy('updated_at', 'desc') + ->groupBy('location_profile_id'); + + if ($request->location_ids != '') { + $vouchers->whereHas('locationProfile', function ($q) use ($request) { + return $q->whereIn('location_id', $request->location_ids); + }); + + $slocations = Location::whereIn('id', $request->location_ids)->get(); - if ($request->location_id != '') { - $vouchers->where('location_id', $request->location_id); + $vouchers = tap($vouchers->paginate(self::LIMIT))->setHidden(['username', 'password']); + } + + if (auth()->guard('customer')->guest() && $request->location_ids != '') { + $vouchers = tap($vouchers->paginate(self::LIMIT))->setHidden(['username', 'password']); } return inertia('Index/Index', [ 'infos' => $infos, 'banners' => $banners, 'locations' => $locations, - 'vouchers' => tap($vouchers->paginate(10))->setHidden(['username', 'password']), - '_location_id' => $request->location_id ?? '', + 'vouchers' => $vouchers, + '_slocations' => $slocations, + '_status' => 0 + ]); + } + + public function favorite() + { + $infos = Info::where('is_publish', 1)->orderBy('updated_at', 'desc')->get(); + $banners = Banner::orderBy('updated_at', 'desc')->get(); + $locations = Location::orderBy('name', 'asc')->get(); + + $vouchers = Voucher::with(['locationProfile.location']) + ->where('is_sold', Voucher::UNSOLD) + ->orderBy('updated_at', 'desc') + ->groupBy('location_profile_id'); + + $customer = Customer::find(auth()->id()); + $vouchers->whereHas('locationProfile', function ($q) use ($customer) { + return $q->whereIn('location_id', $customer->locationFavorites()->pluck('id')->toArray()); + }); + + return inertia('Index/Index', [ + 'infos' => $infos, + 'banners' => $banners, + 'locations' => $locations, + 'vouchers' => tap($vouchers->paginate(self::LIMIT))->setHidden(['username', 'password']), + '_flocations' => $customer->locationFavorites, + '_status' => 1 ]); } @@ -41,6 +93,13 @@ class HomeController extends Controller ]); } + public function addFavorite(Location $location) + { + $customer = Customer::find(auth()->id()); + + $customer->locationFavorites()->toggle([$location->id]); + } + public function notification() { Notification::where('entity_id', auth()->id())->where('is_read', Notification::UNREAD)->update(['is_read' => Notification::READ]); diff --git a/app/Http/Middleware/HandleInertiaCustomerRequests.php b/app/Http/Middleware/HandleInertiaCustomerRequests.php index 690e3d4..6c54c1c 100644 --- a/app/Http/Middleware/HandleInertiaCustomerRequests.php +++ b/app/Http/Middleware/HandleInertiaCustomerRequests.php @@ -48,7 +48,7 @@ class HandleInertiaCustomerRequests extends Middleware 'app_name' => env('APP_NAME', 'App Name'), 'setting' => Setting::getSettings(), 'auth' => [ - 'user' => auth('customer')->user()?->load(['level', 'paylater']), + 'user' => auth('customer')->user()?->load(['level', 'paylater', 'locationFavorites']), ], 'flash' => [ 'message' => fn () => $request->session()->get('message') ?? ['type' => null, 'message' => null], diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 9163cc2..083be54 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -6,6 +6,7 @@ use App\Models\Traits\UserTrackable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Support\Str; @@ -126,7 +127,7 @@ class Customer extends Authenticatable return ' - '; } - return '+62'.$this->phone; + return '+62' . $this->phone; }); } @@ -234,7 +235,7 @@ class Customer extends Authenticatable $paylater = $this->paylaterHistories()->create([ 'credit' => $cut, - 'description' => $deposit->description.' (Pengembalian)', + 'description' => $deposit->description . ' (Pengembalian)', ]); $paylater->update_customer_paylater(); @@ -245,4 +246,9 @@ class Customer extends Authenticatable $deposit->update_customer_balance(); } } + + public function locationFavorites() + { + return $this->belongsToMany(Location::class, CustomerLocationFavorite::class); + } } diff --git a/app/Models/CustomerLocationFavorite.php b/app/Models/CustomerLocationFavorite.php index fd57b6a..20f6b35 100644 --- a/app/Models/CustomerLocationFavorite.php +++ b/app/Models/CustomerLocationFavorite.php @@ -2,10 +2,8 @@ namespace App\Models; -class CustomerLocationFavorite extends Model +use Illuminate\Database\Eloquent\Relations\Pivot; + +class CustomerLocationFavorite extends Pivot { - protected $fillable = [ - 'customer_id', - 'location_id', - ]; } diff --git a/app/Models/LocationProfile.php b/app/Models/LocationProfile.php index 1d2414a..06a8bf3 100644 --- a/app/Models/LocationProfile.php +++ b/app/Models/LocationProfile.php @@ -78,7 +78,7 @@ class LocationProfile extends Model public function diplayExpired(): Attribute { return Attribute::make(get: function () { - return $this->expired.' '.$this->expired_unit; + return $this->expired . ' ' . $this->expired_unit; }); } diff --git a/app/Models/Voucher.php b/app/Models/Voucher.php index 7283c70..7801412 100644 --- a/app/Models/Voucher.php +++ b/app/Models/Voucher.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Support\Facades\Auth; class Voucher extends Model { @@ -25,10 +26,24 @@ class Voucher extends Model protected $appends = [ 'validate_price', 'validate_display_price', + 'discount', 'status', 'created_at_formated' ]; + private static $instance = []; + + private static function getInstance() + { + if (count(self::$instance) == 0) { + self::$instance = [ + 'customer' => Customer::find(auth()->id()) + ]; + } + + return self::$instance; + } + public function locationProfile() { return $this->belongsTo(LocationProfile::class, 'location_profile_id'); @@ -37,14 +52,48 @@ class Voucher extends Model public function validatePrice(): Attribute { return Attribute::make(get: function () { - return ''; + if ($this->locationProfile->prices->count() > 0) { + $price = $this->locationProfile->prices; + if (auth()->guard('customer')->check()) { + $customer = self::getInstance()['customer']; + return $price->where('customer_level_id', $customer->customer_level_id) + ->value('price'); + } + return $price->max('price'); + } + return $this->locationProfile->price; }); } public function validateDisplayPrice(): Attribute { return Attribute::make(get: function () { - return ''; + if ($this->locationProfile->prices->count() > 0) { + $price = $this->locationProfile->prices; + if (auth()->guard('customer')->check()) { + $customer = self::getInstance()['customer']; + return $price->where('customer_level_id', $customer->customer_level_id) + ->value('display_price'); + } + return $price->max('display_price'); + } + return $this->locationProfile->display_price; + }); + } + + public function discount(): Attribute + { + return Attribute::make(get: function () { + if ($this->locationProfile->prices->count() > 0) { + $price = $this->locationProfile->prices; + if (auth()->guard('customer')->check()) { + $customer = self::getInstance()['customer']; + return $price->where('customer_level_id', $customer->customer_level_id) + ->value('discount'); + } + return $price->min('discount'); + } + return $this->locationProfile->discount; }); } diff --git a/database/migrations/2023_06_16_071025_create_customer_location_favorites_table.php b/database/migrations/2023_06_16_071025_create_customer_location_favorites_table.php index fb057cc..d35ad7e 100644 --- a/database/migrations/2023_06_16_071025_create_customer_location_favorites_table.php +++ b/database/migrations/2023_06_16_071025_create_customer_location_favorites_table.php @@ -11,17 +11,12 @@ return new class extends Migration */ public function up(): void { - Schema::create('customer_location_favorites', function (Blueprint $table) { - $table->ulid('id')->primary(); - + Schema::create('customer_location_favorite', function (Blueprint $table) { $table->ulid('location_id')->nullable(); $table->ulid('customer_id')->nullable(); $table->timestamps(); $table->softDeletes(); - $table->ulid('created_by')->nullable(); - $table->ulid('updated_by')->nullable(); - $table->ulid('deleted_by')->nullable(); }); } @@ -30,6 +25,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('customer_location_favorites'); + Schema::dropIfExists('customer_location_favorite'); } }; diff --git a/resources/js/Components/Modal.jsx b/resources/js/Components/Modal.jsx index 2c0b159..86f0622 100644 --- a/resources/js/Components/Modal.jsx +++ b/resources/js/Components/Modal.jsx @@ -1,23 +1,37 @@ -import React from "react"; -import { HiX } from "react-icons/hi"; +import React from 'react' +import { HiX } from 'react-icons/hi' - -export default function Modal({ isOpen, toggle = () => {}, children, title = "", maxW = '2' }) { +export default function Modal({ + isOpen, + toggle = () => {}, + children, + title = '', + maxW = '2', +}) { return ( -
+
-

{ title }

-
-
- {children} -
+
{children}
) -} \ No newline at end of file +} diff --git a/resources/js/Customer/Components/FormLocation.jsx b/resources/js/Customer/Components/FormLocation.jsx index 8df9d99..8a17d57 100644 --- a/resources/js/Customer/Components/FormLocation.jsx +++ b/resources/js/Customer/Components/FormLocation.jsx @@ -1,35 +1,29 @@ -import React from 'react' -import { isEmpty } from 'lodash' -import { HiFilter } from 'react-icons/hi' +import React, { forwardRef } from 'react' +import { HiOutlineFilter, HiOutlineSearch } from 'react-icons/hi' -export default function FormLocation({ - type, - name, - onChange, - value, - label, - autoComplete, - autoFocus, - placeholder, - disabled, - readOnly, - onKeyDownCapture, - max, - min, -}) { - return ( - <> - +const FormLocation = forwardRef( + ( + { + name, + onChange, + value, + autoComplete, + autoFocus, + placeholder, + disabled, + readOnly, + onKeyDownCapture, + max, + min, + }, + ref + ) => { + return (
- +
- - ) -} + ) + } +) + +export default FormLocation diff --git a/resources/js/Customer/Components/Modal.jsx b/resources/js/Customer/Components/Modal.jsx new file mode 100644 index 0000000..dfde8b7 --- /dev/null +++ b/resources/js/Customer/Components/Modal.jsx @@ -0,0 +1,15 @@ +export default function Modal({ isOpen, children }) { + return ( +
+
+
+ {children} +
+
+
+ ) +} diff --git a/resources/js/Customer/Index/Index.jsx b/resources/js/Customer/Index/Index.jsx index 1ccd0af..f47a856 100644 --- a/resources/js/Customer/Index/Index.jsx +++ b/resources/js/Customer/Index/Index.jsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react' +import React from 'react' import { Head, router, usePage } from '@inertiajs/react' +import { HiOutlineBell } from 'react-icons/hi2' +import { handleBanner, ALL, FAVORITE } from './utils' import CustomerLayout from '@/Layouts/CustomerLayout' -import { HiOutlineBell } from 'react-icons/hi2' -import UserBanner from './UserBanner' -import VoucherCard from './VoucherCard' -import FormLocation from '../Components/FormLocation' -import { HiXCircle } from 'react-icons/hi' +import UserBanner from './Partials/UserBanner' +import AllVoucher from './IndexPartials/AllVoucher' +import FavoriteVoucher from './IndexPartials/FavoriteVoucher' const GuestBanner = () => { const { @@ -37,63 +37,32 @@ export default function Index(props) { auth: { user }, infos, banners, - locations, - vouchers: { data, next_page_url }, - _location_id, + _status, } = props - const [locId, setLocId] = useState(_location_id) - const [v, setV] = useState(data) - - const handleBanner = (banner) => { - router.get(route('home.banner', banner)) - } - - const handleSelectLoc = (loc) => { - if (loc.id === locId) { - setLocId('') - fetch('') - return + const isStatus = (s) => { + if (s === _status) { + return 'px-2 py-1 rounded-2xl text-white bg-blue-600 border border-blue-800' } - setLocId(loc.id) - fetch(loc.id) + return 'px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200' } - const handleNextPage = () => { - router.get( - next_page_url, - { - location_id: locId, - }, - { - replace: true, - preserveState: true, - only: ['vouchers'], - onSuccess: (res) => { - setV(v.concat(res.props.vouchers.data)) - }, - } - ) + const handleFavorite = () => { + if (user === null) { + router.visit(route('customer.login')) + } + router.visit(route('customer.home.favorite')) } - const fetch = (locId) => { - router.get( - route(route().current()), - { location_id: locId }, - { - replace: true, - preserveState: true, - onSuccess: (res) => { - setV(res.props.vouchers.data) - }, - } - ) + const handleAll = () => { + router.visit(route('home.index')) } return (
+ {/* guest or user banner */} {user !== null ? : } {/* banner */}
@@ -127,59 +96,21 @@ export default function Index(props) {
-
- -
{/* chips */}
-
+
Semua
- Favorite + Favorit
-
-
Farid Net
-
- -
-
-
- {/*
- {locations.map((location) => ( -
handleSelectLoc(location)} - key={location.id} - className={`px-2 py-1 rounded-2xl ${ - location.id === locId - ? 'text-white bg-blue-600 border border-blue-800' - : 'bg-blue-100 border border-blue-200' - }`} - > - {location.name} -
- ))} -
*/} - - {/* voucher */} -
- {v.map((voucher) => ( - - ))} - {next_page_url !== null && ( -
- Load more -
- )}
+ {_status === ALL && } + {_status === FAVORITE && }
) diff --git a/resources/js/Customer/Index/IndexPartials/AllVoucher.jsx b/resources/js/Customer/Index/IndexPartials/AllVoucher.jsx new file mode 100644 index 0000000..7e70a23 --- /dev/null +++ b/resources/js/Customer/Index/IndexPartials/AllVoucher.jsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react' +import { router, usePage } from '@inertiajs/react' +import { HiXMark } from 'react-icons/hi2' + +import { useModalState } from '@/hooks' +import VoucherCard from '../Partials/VoucherCard' +import FormLocation from '../../Components/FormLocation' +import LocationModal from '../Partials/LocationModal' + +const EmptyLocation = () => { + return ( +
+
Pilih lokasi
+
+ pilih lokasi untuk dapat menampilkan voucher tersedia +
+
+ ) +} + +const EmptyVoucher = () => { + return ( +
+
Voucher belum tersedia
+
+ sepertinya voucher di lokasimu sedang tidak tersedia +
+
+ ) +} + +export default function AllVoucher() { + const { + props: { + locations, + vouchers: { data, next_page_url }, + _slocations, + }, + } = usePage() + + const locationModal = useModalState() + + const nextPageUrl = next_page_url === undefined ? null : next_page_url + const [items, setItems] = useState(data === undefined ? [] : data) + const [sLocations, setSLocations] = useState(_slocations) + + const handleAddLocation = (location) => { + const isExists = sLocations.find((l) => l.id === location.id) + if (!isExists) { + const locations = [location].concat(...sLocations) + setSLocations(locations) + fetch(locations) + } + } + + const handleRemoveLocation = (index) => { + const locations = sLocations.filter((_, i) => i !== index) + setSLocations(locations) + fetch(locations) + } + + const handleNextPage = () => { + let location_ids = sLocations.map((l) => l.id) + + router.get( + nextPageUrl, + { location_ids: location_ids }, + { + replace: true, + preserveState: true, + only: ['vouchers'], + onSuccess: (res) => { + if (res.props.vouchers.data !== undefined) { + setItems(items.concat(res.props.vouchers.data)) + } + }, + } + ) + } + + const fetch = (locations) => { + let location_ids = locations.map((l) => l.id) + + router.get( + route(route().current()), + { location_ids: location_ids }, + { + replace: true, + preserveState: true, + onSuccess: (res) => { + if (res.props.vouchers.data !== undefined) { + setItems(res.props.vouchers.data) + return + } + setItems([]) + }, + } + ) + } + + return ( + <> +
+ +
+
+ {sLocations.map((location, index) => ( +
handleRemoveLocation(index)} + > +
{location.name}
+
+ +
+
+ ))} +
+ {items.length <= 0 && sLocations.length <= 0 && } + + {/* voucher */} +
+ {items.map((voucher) => ( + + ))} + {nextPageUrl !== null && ( +
+ Load more +
+ )} +
+ {items.length <= 0 && sLocations.length > 0 && } + + + + ) +} diff --git a/resources/js/Customer/Index/IndexPartials/FavoriteVoucher.jsx b/resources/js/Customer/Index/IndexPartials/FavoriteVoucher.jsx new file mode 100644 index 0000000..434851c --- /dev/null +++ b/resources/js/Customer/Index/IndexPartials/FavoriteVoucher.jsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import { router, usePage } from '@inertiajs/react' +import { HiOutlineStar } from 'react-icons/hi2' + +import VoucherCard from '../Partials/VoucherCard' + +const EmptyFavorite = () => { + return ( +
+
Favorite kosong
+
+ pilih lokasi favorite mu ya, cek bintangnya +
+
+ ) +} + +const EmptyVoucher = () => { + return ( +
+
Voucher belum tersedia
+
+ sepertinya voucher di lokasimu sedang tidak tersedia +
+
+ ) +} + +export default function FavoriteVoucher() { + const { + props: { + vouchers: { data, next_page_url }, + _flocations, + }, + } = usePage() + + const nextPageUrl = next_page_url === undefined ? null : next_page_url + const [items, setItems] = useState(data === undefined ? [] : data) + + const handleRemoveLocation = (location) => { + router.post( + route('customer.location.favorite', location), + {}, + { + onSuccess: () => { + router.visit(route(route().current())) + }, + } + ) + } + + const handleNextPage = () => { + router.get( + nextPageUrl, + {}, + { + replace: true, + preserveState: true, + only: ['vouchers'], + onSuccess: (res) => { + if (res.props.vouchers.data !== undefined) { + setItems(items.concat(res.props.vouchers.data)) + } + }, + } + ) + } + + useEffect(() => { + setItems(data) + }, [_flocations]) + + return ( + <> +
+ {_flocations.map((location) => ( +
handleRemoveLocation(location)} + > +
{location.name}
+
+ +
+
+ ))} +
+ {_flocations.length <= 0 && } + + {/* voucher */} +
+ {items.map((voucher) => ( + + ))} + {nextPageUrl !== null && ( +
+ Load more +
+ )} +
+ {items.length <= 0 && } + + ) +} diff --git a/resources/js/Customer/Index/BalanceBanner.jsx b/resources/js/Customer/Index/Partials/BalanceBanner.jsx similarity index 100% rename from resources/js/Customer/Index/BalanceBanner.jsx rename to resources/js/Customer/Index/Partials/BalanceBanner.jsx diff --git a/resources/js/Customer/Index/Partials/LocationModal.jsx b/resources/js/Customer/Index/Partials/LocationModal.jsx new file mode 100644 index 0000000..d384518 --- /dev/null +++ b/resources/js/Customer/Index/Partials/LocationModal.jsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from 'react' +import { router, usePage } from '@inertiajs/react' +import { HiArrowLeft, HiOutlineStar } from 'react-icons/hi2' + +import { useAutoFocus } from '@/hooks' +import { isFavorite } from '../utils' +import FormLocation from '../../Components/FormLocation' +import Modal from '../../Components/Modal' + +export default function LocationModal(props) { + const { + props: { + auth: { user }, + }, + } = usePage() + const { state, locations, onItemSelected } = props + + const [search, setSearch] = useState('') + const locationFocus = useAutoFocus() + const [filter_locations, setFilterLocations] = useState(locations) + + const handleOnFilter = (e) => { + setSearch(e.target.value) + if (e.target.value === '') { + setFilterLocations(locations) + return + } + setFilterLocations( + filter_locations.filter((location) => { + let name = location.name.toLowerCase() + let search = e.target.value.toLowerCase() + return name.includes(search) + }) + ) + } + + const handleItemSelected = (location) => { + onItemSelected(location) + state.toggle() + } + + const handleFavorite = (location) => { + router.post(route('customer.location.favorite', location)) + } + + useEffect(() => { + if (state.isOpen === true) { + locationFocus.current.focus() + } + }, [state]) + + return ( + +
+
+ +
+
+ +
+
+
+ {filter_locations.map((location) => ( +
+
handleItemSelected(location)} + className="flex-1 px-3 py-3" + > + {location.name} +
+
handleFavorite(location)} + > + +
+
+ ))} +
+
+ ) +} diff --git a/resources/js/Customer/Index/UserBanner.jsx b/resources/js/Customer/Index/Partials/UserBanner.jsx similarity index 100% rename from resources/js/Customer/Index/UserBanner.jsx rename to resources/js/Customer/Index/Partials/UserBanner.jsx diff --git a/resources/js/Customer/Index/VoucherCard.jsx b/resources/js/Customer/Index/Partials/VoucherCard.jsx similarity index 88% rename from resources/js/Customer/Index/VoucherCard.jsx rename to resources/js/Customer/Index/Partials/VoucherCard.jsx index decaf0b..325cfb0 100644 --- a/resources/js/Customer/Index/VoucherCard.jsx +++ b/resources/js/Customer/Index/Partials/VoucherCard.jsx @@ -12,7 +12,7 @@ export default function VoucherCard({ voucher }) { >
- {voucher.location.name} + {voucher.location_profile.location.name}
@@ -20,7 +20,7 @@ export default function VoucherCard({ voucher }) {
- {voucher.profile} + {voucher.location_profile.display_note}
IDR {formatIDR(voucher.validate_price)} @@ -38,10 +38,10 @@ export default function VoucherCard({ voucher }) {
- {voucher.display_quota} + {voucher.location_profile.quota}
- {voucher.display_expired} + {voucher.location_profile.diplay_expired}
diff --git a/resources/js/Customer/Index/utils.js b/resources/js/Customer/Index/utils.js new file mode 100644 index 0000000..a9eb50b --- /dev/null +++ b/resources/js/Customer/Index/utils.js @@ -0,0 +1,19 @@ +import { router } from '@inertiajs/react' + +export const ALL = 0 +export const FAVORITE = 1 + +export const handleBanner = (banner) => { + router.get(route('home.banner', banner)) +} + +export const isFavorite = (user, id) => { + if (user === null) { + return false + } + const isExists = user.location_favorites.findIndex((f) => f.id === id) + if (isExists !== -1) { + return true + } + return false +} diff --git a/resources/js/Customer/Profile/Index.jsx b/resources/js/Customer/Profile/Index.jsx index 6bc174c..5bfffef 100644 --- a/resources/js/Customer/Profile/Index.jsx +++ b/resources/js/Customer/Profile/Index.jsx @@ -11,7 +11,7 @@ import { import CustomerLayout from '@/Layouts/CustomerLayout' import { useModalState } from '@/hooks' import ModalConfirm from '@/Components/ModalConfirm' -import BalanceBanner from '../Index/BalanceBanner' +import BalanceBanner from '../Index/Partials/BalanceBanner' export default function Index({ auth: { user }, notification_count }) { const confirmModal = useModalState() diff --git a/resources/js/Layouts/CustomerLayout.jsx b/resources/js/Layouts/CustomerLayout.jsx index 8361348..8330dae 100644 --- a/resources/js/Layouts/CustomerLayout.jsx +++ b/resources/js/Layouts/CustomerLayout.jsx @@ -20,7 +20,7 @@ export default function CustomerLayout({ children }) { } = usePage() const handleOnClick = (r) => { - router.get(route(r)) + router.get(route(r, { direct: 1 })) } const isActive = (r) => { diff --git a/resources/js/hooks.js b/resources/js/hooks.js index 2f37452..1873146 100644 --- a/resources/js/hooks.js +++ b/resources/js/hooks.js @@ -1,25 +1,37 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from 'react' + +export const useAutoFocus = () => { + const inputRef = useRef(null) + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + return inputRef +} export function useDebounce(value, delay) { - const [debouncedValue, setDebouncedValue] = useState(value); + const [debouncedValue, setDebouncedValue] = useState(value) useEffect(() => { const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); + setDebouncedValue(value) + }, delay) return () => { - clearTimeout(handler); - }; - }, [value, delay]); - return debouncedValue; + clearTimeout(handler) + } + }, [value, delay]) + return debouncedValue } export function useModalState(state = false) { - const [isOpen, setIsOpen] = useState(state); + const [isOpen, setIsOpen] = useState(state) const toggle = () => { - setIsOpen(!isOpen); - }; + setIsOpen(!isOpen) + } - const [data, setData] = useState(null); + const [data, setData] = useState(null) return { isOpen, @@ -27,7 +39,7 @@ export function useModalState(state = false) { setIsOpen, data, setData, - }; + } } export function usePagination(auth, r) { @@ -37,18 +49,18 @@ export function usePagination(auth, r) { links: [], from: 0, to: 0, - total: 0 + total: 0, }) - const page = data.links.find(link => link.active === true) + const page = data.links.find((link) => link.active === true) const fetch = (page = 1, params = {}) => { setLoading(true) axios .get(route(r, { page: page, ...params }), { headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + auth.user.jwt_token, + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + auth.user.jwt_token, }, }) .then((res) => { @@ -56,7 +68,7 @@ export function usePagination(auth, r) { }) .catch((err) => console.log(err)) .finally(() => setLoading(false)) - }; + } return [data.data, data, page?.label, fetch, loading] -} \ No newline at end of file +} diff --git a/routes/web.php b/routes/web.php index 8dd87a7..2813f22 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,10 @@ Route::middleware(['http_secure_aware', 'guard_should_customer', 'inertia.custom Route::get('/banner/{banner}', [HomeController::class, 'banner'])->name('home.banner'); Route::middleware('auth:customer')->group(function () { + // location to favorite + Route::post('/locations/{location}/add-favorite', [HomeController::class, 'addFavorite'])->name('customer.location.favorite'); + Route::get('/favorites', [HomeController::class, 'favorite'])->name('customer.home.favorite'); + // profile Route::get('profile', [ProfileController::class, 'index'])->name('customer.profile.index'); Route::get('profile/update', [ProfileController::class, 'show'])->name('customer.profile.show'); diff --git a/rsync.sh b/rsync.sh new file mode 100755 index 0000000..2ebc0b4 --- /dev/null +++ b/rsync.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rsync -arP -e 'ssh -p 225' --exclude=node_modules --exclude=database/database.sqlite --exclude=.git --exclude=.env --exclude=public/hot . arm@ajikamaludin.id:/home/arm/projects/www/voucher + +ssh -p 225 arm@ajikamaludin.id -C docker exec php82 php /var/www/voucher/artisan migrate:refresh --seed \ No newline at end of file