home page refactor and customer favorite

dev
Aji Kamaludin 1 year ago
parent 9fc567957d
commit bd2aa6e9b6
No known key found for this signature in database
GPG Key ID: 19058F67F0083AD3

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

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

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

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

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

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

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

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

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

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

@ -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 (
<div className={`${isOpen ? "" : "hidden "} overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 p-4 w-full md:inset-0 h-modal md:h-full justify-center items-center flex bg-opacity-50 dark:bg-opacity-90 bg-gray-900 dark:bg-gray-900`}>
<div
className={`${
isOpen ? '' : 'hidden '
} overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 p-4 w-full md:inset-0 h-modal md:h-full justify-center items-center flex bg-opacity-50 dark:bg-opacity-90 bg-gray-900 dark:bg-gray-900`}
>
<div className={`relative w-full max-w-${maxW}xl h-full md:h-auto`}>
<div className="relative bg-white rounded-lg shadow dark:bg-gray-700 text-base dark:text-gray-400">
<div className="flex items-start justify-between rounded-t dark:border-gray-600 p-2">
<h3 className="text-xl font-medium text-gray-900 dark:text-white py-2 pl-2">{ title }</h3>
<button aria-label="Close" className="ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white" type="button" onClick={toggle}>
<HiX className="h-5 w-5"/>
<h3 className="text-xl font-medium text-gray-900 dark:text-white py-2 pl-2">
{title}
</h3>
<button
aria-label="Close"
className="ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
type="button"
onClick={toggle}
>
<HiX className="h-5 w-5" />
</button>
</div>
<div className="px-4 pb-4 space-y-2">
{children}
</div>
<div className="px-4 pb-4 space-y-2">{children}</div>
</div>
</div>
</div>
)
}
}

@ -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 (
<>
<label
htmlFor={name}
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>
{label}
</label>
const FormLocation = forwardRef(
(
{
name,
onChange,
value,
autoComplete,
autoFocus,
placeholder,
disabled,
readOnly,
onKeyDownCapture,
max,
min,
},
ref
) => {
return (
<div className="relative">
<input
id={name}
type="text"
className="mb-2 bg-gray-50 border text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:placeholder-gray-400 dark:text-white active:ring-blue-500 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-500 dark:focus:border-blue-500"
className="bg-gray-50 border text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:placeholder-gray-400 dark:text-white active:ring-blue-500 focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-500 dark:focus:border-blue-500"
onChange={onChange}
name={name}
value={value}
@ -41,11 +35,14 @@ export default function FormLocation({
onKeyDownCapture={onKeyDownCapture}
max={max}
min={min}
ref={ref}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<HiFilter />
<HiOutlineSearch className="h-6 w-6" />
</div>
</div>
</>
)
}
)
}
)
export default FormLocation

@ -0,0 +1,15 @@
export default function Modal({ isOpen, children }) {
return (
<div
className={`${
isOpen ? '' : 'hidden '
} overflow-y-auto overflow-x-hidden fixed top-0 right-0 z-50 w-full h-full justify-center flex bg-opacity-50 dark:bg-opacity-90 bg-gray-900 dark:bg-gray-900 delay-150 transition ease-in-out duration-1000`}
>
<div className={`relative w-full max-w-md h-full md:h-auto`}>
<div className="relative bg-white h-full p-2 dark:bg-gray-700 text-base dark:text-gray-400">
{children}
</div>
</div>
</div>
)
}

@ -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 (
<CustomerLayout>
<Head title="Home" />
<div className="flex flex-col min-h-[calc(95dvh)]">
{/* guest or user banner */}
{user !== null ? <UserBanner user={user} /> : <GuestBanner />}
{/* banner */}
<div className="w-full">
@ -127,59 +96,21 @@ export default function Index(props) {
</div>
<div className="w-full flex flex-col">
<div className="w-full space-x-2 px-2 mb-2">
<FormLocation placeholder="Cari Lokasi" value={''} />
</div>
{/* chips */}
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4">
<div
className={`px-2 py-1 rounded-2xl text-white bg-blue-600 border border-blue-800`}
>
<div className={isStatus(ALL)} onClick={handleAll}>
Semua
</div>
<div
className={`px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200`}
className={isStatus(FAVORITE)}
onClick={handleFavorite}
>
Favorite
Favorit
</div>
<div className="flex flex-row items-center gap-1 px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200">
<div>Farid Net</div>
<div>
<HiXCircle className="h-5 w-5 text-red-700" />
</div>
</div>
</div>
{/* <div className="w-full flex flex-row overflow-y-scroll space-x-2 px-2">
{locations.map((location) => (
<div
onClick={() => 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}
</div>
))}
</div> */}
{/* voucher */}
<div className="flex flex-col w-full px-3 mt-3 space-y-2">
{v.map((voucher) => (
<VoucherCard key={voucher.id} voucher={voucher} />
))}
{next_page_url !== null && (
<div
onClick={handleNextPage}
className="w-full text-center px-2 py-1 border mt-5 hover:bg-blue-600 hover:text-white"
>
Load more
</div>
)}
</div>
</div>
{_status === ALL && <AllVoucher />}
{_status === FAVORITE && <FavoriteVoucher />}
</div>
</CustomerLayout>
)

@ -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 (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">Pilih lokasi</div>
<div className="text-gray-400">
pilih lokasi untuk dapat menampilkan voucher tersedia
</div>
</div>
)
}
const EmptyVoucher = () => {
return (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">Voucher belum tersedia</div>
<div className="text-gray-400">
sepertinya voucher di lokasimu sedang tidak tersedia
</div>
</div>
)
}
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 (
<>
<div
className="w-full space-x-2 px-4 mt-2"
onClick={locationModal.toggle}
>
<FormLocation placeholder="Cari Lokasi" />
</div>
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4 mt-2">
{sLocations.map((location, index) => (
<div
className="flex flex-row items-center gap-1 px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200"
key={location.id}
onClick={() => handleRemoveLocation(index)}
>
<div>{location.name}</div>
<div className="pl-2">
<HiXMark className="h-5 w-5 text-red-700" />
</div>
</div>
))}
</div>
{items.length <= 0 && sLocations.length <= 0 && <EmptyLocation />}
{/* voucher */}
<div className="flex flex-col w-full px-3 mt-3 space-y-2">
{items.map((voucher) => (
<VoucherCard key={voucher.id} voucher={voucher} />
))}
{nextPageUrl !== null && (
<div
onClick={handleNextPage}
className="w-full text-center px-2 py-1 border mt-5 hover:bg-blue-600 hover:text-white"
>
Load more
</div>
)}
</div>
{items.length <= 0 && sLocations.length > 0 && <EmptyVoucher />}
<LocationModal
state={locationModal}
locations={locations}
onItemSelected={handleAddLocation}
/>
</>
)
}

@ -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 (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">Favorite kosong</div>
<div className="text-gray-400">
pilih lokasi favorite mu ya, cek bintangnya
</div>
</div>
)
}
const EmptyVoucher = () => {
return (
<div className="w-full px-5 text-center flex flex-col my-auto">
<div className="font-bold text-xl">Voucher belum tersedia</div>
<div className="text-gray-400">
sepertinya voucher di lokasimu sedang tidak tersedia
</div>
</div>
)
}
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 (
<>
<div className="w-full flex flex-row overflow-y-scroll space-x-2 px-4 mt-2">
{_flocations.map((location) => (
<div
className="flex flex-row items-center gap-1 px-2 py-1 rounded-2xl bg-blue-100 border border-blue-200"
key={location.id}
onClick={() => handleRemoveLocation(location)}
>
<div>{location.name}</div>
<div className="pl-2">
<HiOutlineStar className="h-5 w-5 text-yellow-300 fill-yellow-300" />
</div>
</div>
))}
</div>
{_flocations.length <= 0 && <EmptyFavorite />}
{/* voucher */}
<div className="flex flex-col w-full px-3 mt-3 space-y-2">
{items.map((voucher) => (
<VoucherCard key={voucher.id} voucher={voucher} />
))}
{nextPageUrl !== null && (
<div
onClick={handleNextPage}
className="w-full text-center px-2 py-1 border mt-5 hover:bg-blue-600 hover:text-white"
>
Load more
</div>
)}
</div>
{items.length <= 0 && <EmptyVoucher />}
</>
)
}

@ -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 (
<Modal isOpen={state.isOpen}>
<div className="flex flex-row items-center mb-4">
<div className="pr-2 py-2" onClick={state.toggle}>
<HiArrowLeft className="w-6 h-6" />
</div>
<div className="flex-1">
<FormLocation
placeholder="Cari Lokasi"
ref={locationFocus}
value={search}
onChange={handleOnFilter}
/>
</div>
</div>
<div className="flex flex-col overflow-y-auto max-h-[80vh] bg-white">
{filter_locations.map((location) => (
<div
className="flex flex-row justify-between items-center"
key={location.id}
>
<div
onClick={() => handleItemSelected(location)}
className="flex-1 px-3 py-3"
>
{location.name}
</div>
<div
className={`px-3 py-2 ${
user === null ? 'hidden' : ''
}`}
onClick={() => handleFavorite(location)}
>
<HiOutlineStar
className={`w-7 h-7 ${
isFavorite(user, location.id)
? 'text-yellow-300 fill-yellow-300'
: 'text-gray-300'
}`}
/>
</div>
</div>
))}
</div>
</Modal>
)
}

@ -12,7 +12,7 @@ export default function VoucherCard({ voucher }) {
>
<div className="w-full flex flex-row justify-between">
<div className="text-base font-bold">
{voucher.location.name}
{voucher.location_profile.location.name}
</div>
<div className="text-sm text-gray-500"></div>
</div>
@ -20,7 +20,7 @@ export default function VoucherCard({ voucher }) {
<div className="flex flex-row justify-between items-center">
<div>
<div className="text-xs text-gray-400 py-1">
{voucher.profile}
{voucher.location_profile.display_note}
</div>
<div className="text-xl font-bold">
IDR {formatIDR(voucher.validate_price)}
@ -38,10 +38,10 @@ export default function VoucherCard({ voucher }) {
</div>
<div className="flex flex-col justify-end text-right">
<div className="text-3xl font-bold">
{voucher.display_quota}
{voucher.location_profile.quota}
</div>
<div className="text-gray-400 ">
{voucher.display_expired}
{voucher.location_profile.diplay_expired}
</div>
</div>
</div>

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

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

@ -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) => {

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

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

@ -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
Loading…
Cancel
Save