language login

This commit is contained in:
Bayu Lukman Yusuf 2026-01-13 14:56:47 +07:00
parent 9c8ebdfabe
commit d21b26a5d2
15 changed files with 111 additions and 83 deletions

View File

@ -5,10 +5,9 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Repositories\Member\Auth\MemberAuthRepository; use App\Repositories\Member\Auth\MemberAuthRepository;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class LoginWaController extends Controller class LoginWaController extends Controller
{ {
@ -36,13 +35,23 @@ class LoginWaController extends Controller
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => __('otp.invalid_phone'), 'message' => __('otp.invalid_phone'),
'errors' => $validator->errors() 'errors' => $validator->errors(),
], 422); ], 422);
} }
$identity = $request->identity; $identity = $request->identity;
// check first if user exists
$user = $this->memberAuthRepository->check(['phone' => $identity]);
if (! $user) {
return response()->json([
'success' => false,
'message' => __('otp.user_not_found'),
], 404);
}
try { try {
// Use MemberAuthRepository to generate OTP // Use MemberAuthRepository to generate OTP
$otp = $this->memberAuthRepository->waOtp(['phone' => $identity]); $otp = $this->memberAuthRepository->waOtp(['phone' => $identity]);
@ -53,14 +62,14 @@ class LoginWaController extends Controller
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'message' => __('otp.sent'), 'message' => __('otp.sent'),
'redirect' => route('login-phone.otp.view', ['identity' => $identity]) 'redirect' => route('login-phone.otp.view', ['identity' => $identity]),
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error("OTP generation failed: " . $e->getMessage()); Log::error('OTP generation failed: '.$e->getMessage());
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => __('otp.generate_failed') 'message' => __('otp.generate_failed'),
], 500); ], 500);
} }
} }
@ -68,7 +77,7 @@ class LoginWaController extends Controller
public function otpView($identity) public function otpView($identity)
{ {
return view('account.otp', [ return view('account.otp', [
'identity' => $identity 'identity' => $identity,
]); ]);
} }
@ -92,25 +101,23 @@ class LoginWaController extends Controller
// Use MemberAuthRepository to verify OTP // Use MemberAuthRepository to verify OTP
$result = $this->memberAuthRepository->waOtpConfirm([ $result = $this->memberAuthRepository->waOtpConfirm([
'phone' => $identity, 'phone' => $identity,
'otp' => $otp 'otp' => $otp,
]); ]);
// TODO: Authenticate user or create new user $check = $this->memberAuthRepository->check(['phone' => $identity]);
// For now, we'll just redirect to dashboard
// In production, you would:
// 1. Find or create user by phone number // Auth::guard('web')->attempt(['id' => $check->id]);
// 2. Log them in
// 3. Redirect to intended page
return redirect()->route('home')->with('success', __('otp.login_success')); return redirect()->route('home')->with('success', __('otp.login_success'));
} catch (\Illuminate\Validation\ValidationException $e) { } catch (\Illuminate\Validation\ValidationException $e) {
return back() return back()
->withErrors(['otp' => $e->getMessage()]) ->withErrors(['otp' => $e->getMessage()])
->withInput(); ->withInput();
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error("OTP verification failed: " . $e->getMessage()); Log::error('OTP verification failed: '.$e->getMessage());
return back() return back()
->withErrors(['otp' => __('otp.verification_failed')]) ->withErrors(['otp' => __('otp.verification_failed')])
->withInput(); ->withInput();

View File

@ -27,10 +27,10 @@ class MemberAuthRepository
$this->voucherRepository = $voucherRepository; $this->voucherRepository = $voucherRepository;
} }
public function check($request) public function check($data)
{ {
$phone = $request->phone; $phone = $data['phone'] ?? null;
$email = $request->email; $email = $data['email'] ?? null;
if ($email) { if ($email) {
return $this->findUserByEmail($email); return $this->findUserByEmail($email);

View File

@ -1,4 +1,6 @@
<?php <?php
return [ return [
'categories' => 'Categories', 'categories' => 'Categories',
'account' => 'Account',
'wishlist' => 'Wishlist',
]; ];

View File

@ -23,6 +23,7 @@ return [
'resend_in' => 'Resend in', 'resend_in' => 'Resend in',
'need_help' => 'Need help?', 'need_help' => 'Need help?',
'invalid_phone' => 'Please enter a valid phone number', 'invalid_phone' => 'Please enter a valid phone number',
'user_not_found' => 'User not found with this phone number',
'sent' => 'OTP sent successfully', 'sent' => 'OTP sent successfully',
'expired' => 'OTP has expired', 'expired' => 'OTP has expired',
'max_attempts' => 'Maximum attempts reached. Please request a new OTP', 'max_attempts' => 'Maximum attempts reached. Please request a new OTP',

View File

@ -31,6 +31,8 @@ return [
'facebook' => 'Facebook', 'facebook' => 'Facebook',
'apple' => 'Apple', 'apple' => 'Apple',
'need_help' => 'Need help?', 'need_help' => 'Need help?',
'rights_reserved' => '© All rights reserved. Made by :company', 'rights_reserved' => '© All rights reserved. Made by <span class="animate-underline"><a class="animate-target text-dark-emphasis text-decoration-none" href="/" rel="noreferrer" target="_blank">:company</a></span>',
'company_name' => 'Coderthemes',
'sending' => 'Sending...',
'error' => 'An error occurred. Please try again.',
]; ];

View File

@ -46,7 +46,6 @@ return [
'apple' => 'Apple', 'apple' => 'Apple',
'need_help' => 'Need help?', 'need_help' => 'Need help?',
'rights_reserved' => '© All rights reserved. Made by :company', 'rights_reserved' => '© All rights reserved. Made by :company',
'company_name' => 'Coderthemes',
// Benefits section // Benefits section
'benefits_title' => 'Why create an account?', 'benefits_title' => 'Why create an account?',

View File

@ -1,4 +1,6 @@
<?php <?php
return [ return [
'categories' => 'Kategori', 'categories' => 'Kategori',
'account' => 'Akun',
'wishlist' => 'Daftar Keinginan',
]; ];

36
lang/id/otp.php Normal file
View File

@ -0,0 +1,36 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| OTP Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during OTP authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'title' => 'Verifikasi Telepon Anda',
'description' => 'Kami mengirim kode 6 digit ke :phone',
'enter_code' => 'Masukkan kode verifikasi',
'invalid_code' => 'Masukkan kode 6 digit yang valid',
'verify' => 'Verifikasi',
'back_to_login' => 'Kembali ke login',
'resend_code' => 'Kirim ulang kode',
'resend_in' => 'Kirim ulang dalam',
'need_help' => 'Butuh bantuan?',
'invalid_phone' => 'Masukkan nomor telepon yang valid',
'user_not_found' => 'Pengguna tidak ditemukan dengan nomor telepon ini',
'sent' => 'OTP berhasil dikirim',
'expired' => 'OTP telah kadaluarsa',
'max_attempts' => 'Maksimal percobaan tercapai. Silakan minta OTP baru',
'invalid' => 'Kode OTP tidak valid',
'login_success' => 'Login berhasil',
'resend_failed' => 'Gagal mengirim ulang OTP. Silakan coba lagi',
'generate_failed' => 'Gagal membuat OTP. Silakan coba lagi',
'verification_failed' => 'Verifikasi OTP gagal. Silakan coba lagi',
];

View File

@ -31,7 +31,7 @@ return [
'facebook' => 'Facebook', 'facebook' => 'Facebook',
'apple' => 'Apple', 'apple' => 'Apple',
'need_help' => 'Butuh bantuan?', 'need_help' => 'Butuh bantuan?',
'rights_reserved' => ' Semua hak dilindungi. Dibuat oleh :company', 'rights_reserved' => '© Semua hak dilindungi. Dibuat oleh <span class="animate-underline"><a class="animate-target text-dark-emphasis text-decoration-none" href="https://coderthemes.com/" rel="noreferrer" target="_blank">:company</a></span>',
'rights_reserved' => '© Semua hak dilindungi. Dibuat oleh :company', 'sending' => 'Mengirim...',
'company_name' => 'Coderthemes', 'error' => 'Terjadi kesalahan. Silakan coba lagi.',
]; ];

View File

@ -46,7 +46,6 @@ return [
'apple' => 'Apple', 'apple' => 'Apple',
'need_help' => 'Butuh bantuan?', 'need_help' => 'Butuh bantuan?',
'rights_reserved' => ' Semua hak dilindungi. Dibuat oleh :company', 'rights_reserved' => ' Semua hak dilindungi. Dibuat oleh :company',
'company_name' => 'Coderthemes',
// Benefits section // Benefits section
'benefits_title' => 'Mengapa membuat akun?', 'benefits_title' => 'Mengapa membuat akun?',

View File

@ -35,7 +35,7 @@ $grays: () !default;
$min-contrast-ratio: 2 !default; $min-contrast-ratio: 2 !default;
// Theme colors // Theme colors
$primary: #f55266 !default; $primary: #000000 !default;
$secondary: $gray-500 !default; $secondary: $gray-500 !default;
$success: #33b36b !default; $success: #33b36b !default;
$info: #2f6ed5 !default; $info: #2f6ed5 !default;

View File

@ -9,23 +9,7 @@
<header class="navbar px-0 pb-4 mt-n2 mt-sm-0 mb-2 mb-md-3 mb-lg-4"> <header class="navbar px-0 pb-4 mt-n2 mt-sm-0 mb-2 mb-md-3 mb-lg-4">
<a class="navbar-brand pt-0" href="/"> <a class="navbar-brand pt-0" href="/">
<span class="d-flex flex-shrink-0 text-primary me-2"> <span class="d-flex flex-shrink-0 text-primary me-2">
<svg height="36" width="36" xmlns="http://www.w3.org/2000/svg"> <img src="{{ asset('logo/logo-colored.png') }}" alt="AsiaGolf Logo" style="height: 36px; width: auto;">
<path
d="M36 18.01c0 8.097-5.355 14.949-12.705 17.2a18.12 18.12 0 0 1-5.315.79C9.622 36 2.608 30.313.573 22.611.257 21.407.059 20.162 0 18.879v-1.758c.02-.395.059-.79.099-1.185.099-.908.277-1.817.514-2.686C2.687 5.628 9.682 0 18 0c5.572 0 10.551 2.528 13.871 6.517 1.502 1.797 2.648 3.91 3.359 6.201.494 1.659.771 3.436.771 5.292z"
fill="currentColor"></path>
<g fill="#fff">
<path
d="M17.466 21.624c-.514 0-.988-.316-1.146-.829-.198-.632.138-1.303.771-1.501l7.666-2.469-1.205-8.254-13.317 4.621a1.19 1.19 0 0 1-1.521-.75 1.19 1.19 0 0 1 .751-1.521l13.89-4.818c.553-.197 1.166-.138 1.64.158a1.82 1.82 0 0 1 .85 1.284l1.344 9.183c.138.987-.494 1.994-1.482 2.33l-7.864 2.528-.375.04zm7.31.138c-.178-.632-.85-1.007-1.482-.81l-5.177 1.58c-2.331.79-3.28.02-3.418-.099l-6.56-8.412a4.25 4.25 0 0 0-4.406-1.758l-3.122.987c-.237.889-.415 1.777-.514 2.686l4.228-1.363a1.84 1.84 0 0 1 1.857.81l6.659 8.551c.751.948 2.015 1.323 3.359 1.323.909 0 1.857-.178 2.687-.474l5.078-1.54c.632-.178 1.008-.829.81-1.481z">
</path>
<use href="#czlogo"></use>
<use href="#czlogo" x="8.516" y="-2.172"></use>
</g>
<defs>
<path
d="M18.689 28.654a1.94 1.94 0 0 1-1.936 1.935 1.94 1.94 0 0 1-1.936-1.935 1.94 1.94 0 0 1 1.936-1.935 1.94 1.94 0 0 1 1.936 1.935z"
id="czlogo"></path>
</defs>
</svg>
</span> </span>
AsiaGolf AsiaGolf
</a> </a>
@ -81,9 +65,7 @@
href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('otp.need_help') }}</a> href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('otp.need_help') }}</a>
</div> </div>
<p class="fs-xs mb-0"> <p class="fs-xs mb-0">
© All rights reserved. Made by <span class="animate-underline"><a {!! __('signin.rights_reserved', ['company' => config('app.name')]) !!}
class="animate-target text-dark-emphasis text-decoration-none"
href="https://coderthemes.com/" rel="noreferrer" target="_blank">{{ __('signin.company_name') }}</a></span>
</p> </p>
</footer> </footer>
</div> </div>

View File

@ -9,23 +9,7 @@
<header class="navbar px-0 pb-4 mt-n2 mt-sm-0 mb-2 mb-md-3 mb-lg-4"> <header class="navbar px-0 pb-4 mt-n2 mt-sm-0 mb-2 mb-md-3 mb-lg-4">
<a class="navbar-brand pt-0" href="/"> <a class="navbar-brand pt-0" href="/">
<span class="d-flex flex-shrink-0 text-primary me-2"> <span class="d-flex flex-shrink-0 text-primary me-2">
<svg height="36" width="36" xmlns="http://www.w3.org/2000/svg"> <img src="{{ asset('logo/logo-colored.png') }}" alt="AsiaGolf Logo" style="height: 36px; width: auto;">
<path
d="M36 18.01c0 8.097-5.355 14.949-12.705 17.2a18.12 18.12 0 0 1-5.315.79C9.622 36 2.608 30.313.573 22.611.257 21.407.059 20.162 0 18.879v-1.758c.02-.395.059-.79.099-1.185.099-.908.277-1.817.514-2.686C2.687 5.628 9.682 0 18 0c5.572 0 10.551 2.528 13.871 6.517 1.502 1.797 2.648 3.91 3.359 6.201.494 1.659.771 3.436.771 5.292z"
fill="currentColor"></path>
<g fill="#fff">
<path
d="M17.466 21.624c-.514 0-.988-.316-1.146-.829-.198-.632.138-1.303.771-1.501l7.666-2.469-1.205-8.254-13.317 4.621a1.19 1.19 0 0 1-1.521-.75 1.19 1.19 0 0 1 .751-1.521l13.89-4.818c.553-.197 1.166-.138 1.64.158a1.82 1.82 0 0 1 .85 1.284l1.344 9.183c.138.987-.494 1.994-1.482 2.33l-7.864 2.528-.375.04zm7.31.138c-.178-.632-.85-1.007-1.482-.81l-5.177 1.58c-2.331.79-3.28.02-3.418-.099l-6.56-8.412a4.25 4.25 0 0 0-4.406-1.758l-3.122.987c-.237.889-.415 1.777-.514 2.686l4.228-1.363a1.84 1.84 0 0 1 1.857.81l6.659 8.551c.751.948 2.015 1.323 3.359 1.323.909 0 1.857-.178 2.687-.474l5.078-1.54c.632-.178 1.008-.829.81-1.481z">
</path>
<use href="#czlogo"></use>
<use href="#czlogo" x="8.516" y="-2.172"></use>
</g>
<defs>
<path
d="M18.689 28.654a1.94 1.94 0 0 1-1.936 1.935 1.94 1.94 0 0 1-1.936-1.935 1.94 1.94 0 0 1 1.936-1.935 1.94 1.94 0 0 1 1.936 1.935z"
id="czlogo"></path>
</defs>
</svg>
</span> </span>
AsiaGolf AsiaGolf
</a> </a>
@ -36,10 +20,14 @@
<a class="nav-link text-decoration-underline p-0 ms-2" <a class="nav-link text-decoration-underline p-0 ms-2"
href="{{ route('register') }}">{{ __('signin.create_account') }}</a> href="{{ route('register') }}">{{ __('signin.create_account') }}</a>
</div> </div>
{{-- show error message --}}
<div id="error-message" class="alert alert-danger d-none"></div>
<!-- Form --> <!-- Form -->
<form class="needs-validation" id="loginForm" novalidate=""> <form class="needs-validation" id="loginForm" novalidate="">
<div class="position-relative mb-4"> <div class="position-relative mb-4">
<input class="form-control form-control-lg" placeholder="{{ $type == 'email' ? __('signin.email_placeholder') : __('signin.phone_placeholder') }}" required="" name="identity" id="identity" /> <input class="form-control form-control-lg" placeholder="{{ $type == 'email' ? __('signin.email_placeholder') : __('signin.phone_placeholder') }}" required="" name="identity" id="identity" min="1" type="{{ $type == 'email' ? 'email' :'number' }}" />
<div class="invalid-tooltip bg-transparent py-0">{{ $type == 'email' ? __('signin.email_invalid') : __('signin.phone_invalid') }}</div> <div class="invalid-tooltip bg-transparent py-0">{{ $type == 'email' ? __('signin.email_invalid') : __('signin.phone_invalid') }}</div>
</div> </div>
@ -65,9 +53,7 @@
href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('signin.need_help') }}</a> href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('signin.need_help') }}</a>
</div> </div>
<p class="fs-xs mb-0"> <p class="fs-xs mb-0">
© All rights reserved. Made by <span class="animate-underline"><a {!! __('signin.rights_reserved', ['company' => config('app.name')]) !!}
class="animate-target text-dark-emphasis text-decoration-none"
href="https://coderthemes.com/" rel="noreferrer" target="_blank">{{ __('signin.company_name') }}</a></span>
</p> </p>
</footer> </footer>
</div> </div>
@ -94,9 +80,19 @@ document.addEventListener('DOMContentLoaded', function() {
const submitBtn = loginForm.querySelector('button[type="submit"]'); const submitBtn = loginForm.querySelector('button[type="submit"]');
const identityInput = document.getElementById('identity'); const identityInput = document.getElementById('identity');
// Hide error message when user starts typing
identityInput.addEventListener('input', function() {
const errorDiv = document.getElementById('error-message');
errorDiv.classList.add('d-none');
});
loginForm.addEventListener('submit', function(e) { loginForm.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
// Hide error message when submitting
const errorDiv = document.getElementById('error-message');
errorDiv.classList.add('d-none');
// Reset validation // Reset validation
loginForm.classList.remove('was-validated'); loginForm.classList.remove('was-validated');
@ -131,13 +127,17 @@ document.addEventListener('DOMContentLoaded', function() {
// Redirect to OTP verification page // Redirect to OTP verification page
window.location.href = data.redirect; window.location.href = data.redirect;
} else { } else {
// Show error // Show error in error message div
alert(data.message || '{{ __("signin.error") }}'); const errorDiv = document.getElementById('error-message');
errorDiv.textContent = data.message || '{{ __("signin.error") }}';
errorDiv.classList.remove('d-none');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert('{{ __("signin.error") }}'); const errorDiv = document.getElementById('error-message');
errorDiv.textContent = '{{ __("signin.error") }}';
errorDiv.classList.remove('d-none');
}) })
.finally(() => { .finally(() => {
// Reset button state // Reset button state

View File

@ -126,9 +126,7 @@
href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('signup.need_help') }}</a> href="{{ route('second', ['help', 'topics-v1']) }}">{{ __('signup.need_help') }}</a>
</div> </div>
<p class="fs-xs mb-0"> <p class="fs-xs mb-0">
© All rights reserved. Made by <span class="animate-underline"><a {!! __('signin.rights_reserved', ['company' => config('app.name')]) !!}
class="animate-target text-dark-emphasis text-decoration-none"
href="https://coderthemes.com/" rel="noreferrer" target="blank">{{ __('signup.company_name') }}</a></span>
</p> </p>
</footer> </footer>
</div> </div>

View File

@ -235,16 +235,16 @@
<!-- Account button visible on screens > 768px wide (md breakpoint) --> <!-- Account button visible on screens > 768px wide (md breakpoint) -->
<a class="btn btn-icon btn-lg fs-lg btn-outline-secondary border-0 rounded-circle animate-shake d-none d-md-inline-flex" <a class="btn btn-icon btn-lg fs-lg btn-outline-secondary border-0 rounded-circle animate-shake d-none d-md-inline-flex"
href="{{ route('register') }}"> href="{{ route('login') }}">
<i class="ci-user animate-target"></i> <i class="ci-user animate-target"></i>
<span class="visually-hidden">Account</span> <span class="visually-hidden">{{ __('header.account') }}</span>
</a> </a>
<!-- Wishlist button visible on screens > 768px wide (md breakpoint) --> <!-- Wishlist button visible on screens > 768px wide (md breakpoint) -->
<a class="btn btn-icon btn-lg fs-lg btn-outline-secondary border-0 rounded-circle animate-pulse d-none d-md-inline-flex" <a class="btn btn-icon btn-lg fs-lg btn-outline-secondary border-0 rounded-circle animate-pulse d-none d-md-inline-flex"
href="#!"> href="#!">
<i class="ci-heart animate-target"></i> <i class="ci-heart animate-target"></i>
<span class="visually-hidden">Wishlist</span> <span class="visually-hidden">{{ __('header.wishlist') }}</span>
</a> </a>
<!-- Cart button --> <!-- Cart button -->
@ -301,13 +301,13 @@
<!-- Account and Wishlist buttons visible on screens < 768px wide (md breakpoint) --> <!-- Account and Wishlist buttons visible on screens < 768px wide (md breakpoint) -->
<div class="offcanvas-header border-top px-0 py-3 mt-3 d-md-none"> <div class="offcanvas-header border-top px-0 py-3 mt-3 d-md-none">
<div class="nav nav-justified w-100"> <div class="nav nav-justified w-100">
<a class="nav-link border-end" href="{{ route('second', ['account', 'signin']) }}"> <a class="nav-link border-end" href="{{ route('login') }}">
<i class="ci-user fs-lg opacity-60 me-2"></i> <i class="ci-user fs-lg opacity-60 me-2"></i>
Account {{ __('header.account') }}
</a> </a>
<a class="nav-link" href="#!"> <a class="nav-link" href="#!">
<i class="ci-heart fs-lg opacity-60 me-2"></i> <i class="ci-heart fs-lg opacity-60 me-2"></i>
Wishlist {{ __('header.wishlist') }}
</a> </a>
</div> </div>
</div> </div>