Compare commits
60 Commits
snyk-fix-f
...
shift-1618
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7d5febdf | ||
| 44f359ff9c | |||
| 20f36d519a | |||
| e358e9fb5d | |||
| b882d92328 | |||
| 3257aa9ee9 | |||
| 0bcd6f5e86 | |||
| 75d958856a | |||
| 71eb00d010 | |||
| eab3d062f5 | |||
| 1afa22e2f4 | |||
| b85d039c36 | |||
| c1a4fd13d5 | |||
| 9a1ffe835c | |||
| c3b9482d35 | |||
| bc8f9149dc | |||
| c60213257b | |||
| 6a78ba2bb2 | |||
| a5f7ce8393 | |||
| 4e1505c5c2 | |||
| e967bdde71 | |||
| 74e9e39722 | |||
| 0df4033fca | |||
| e02770cc85 | |||
| 3687af2656 | |||
| b168931266 | |||
| b669dd319e | |||
| e37b9a30a4 | |||
| 436d4b8acf | |||
| a2eb1d5d1b | |||
| be4fdb2f80 | |||
| 538f324ff4 | |||
| 59ca73519d | |||
| 6bc2b888a4 | |||
| be8b2d48b3 | |||
| 5f631a5c3d | |||
| fea3756eab | |||
| 6d8db2cd80 | |||
| 9725f4944f | |||
| 9b1b92d0cf | |||
| b10b6b712e | |||
| db018e9120 | |||
| 1444bc9aa4 | |||
| 9e7fc79fa1 | |||
| 06460d9677 | |||
| beed9f9c11 | |||
| 38b3d5d367 | |||
| ad080b19a2 | |||
| 274d9759b6 | |||
| d992570ee8 | |||
| d72c08b4c9 | |||
| 7baea36628 | |||
| b20c79b679 | |||
| 5cbebd8840 | |||
| d36979cbbd | |||
| 1c28cd7902 | |||
| df19e43112 | |||
| 5a65517d2b | |||
| 49eb388041 | |||
| 659ae2e3ac |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,7 +15,7 @@ app/storage/
|
||||
|
||||
# Laravel 5 & Lumen specific
|
||||
public/storage
|
||||
public/hot
|
||||
public/hot*
|
||||
|
||||
# Laravel 5 & Lumen specific with changed public path
|
||||
public_html/storage
|
||||
@@ -259,3 +259,4 @@ phpcbf.phar
|
||||
|
||||
### PHPStorm ###
|
||||
.idea/
|
||||
|
||||
|
||||
4
.shift
Normal file
4
.shift
Normal file
@@ -0,0 +1,4 @@
|
||||
This file was added by Shift #161875 in order to open a
|
||||
Pull Request since no other commits were made.
|
||||
|
||||
You should remove this file.
|
||||
@@ -6,10 +6,11 @@ use App\Helpers;
|
||||
use App\Jobs\SendEmail;
|
||||
use App\Mail\UserEmailUpdateRequest;
|
||||
use App\Models\User;
|
||||
use App\Providers\QRCodeProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use RobThree\Auth\Algorithm;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
@@ -50,11 +51,11 @@ class AccountController extends Controller
|
||||
'email' => ['required', 'email', 'unique:users,email,' . $user->id],
|
||||
'phone' => 'required',
|
||||
|
||||
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
|
||||
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
|
||||
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
|
||||
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
|
||||
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
|
||||
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
|
||||
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
|
||||
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
|
||||
|
||||
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
|
||||
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
|
||||
@@ -68,11 +69,11 @@ class AccountController extends Controller
|
||||
'email.email' => __('validation.custom_messages.email_invalid'),
|
||||
'phone.required' => __('validation.custom_messages.phone_required'),
|
||||
|
||||
'home_address.required' => __('validation.custom_messages.home_address_required'),
|
||||
'home_city.required' => __('validation.custom_messages.home_city_required'),
|
||||
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
|
||||
'home_country.required' => __('validation.custom_messages.home_country_required'),
|
||||
'home_state.required' => __('validation.custom_messages.home_state_required'),
|
||||
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
|
||||
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
|
||||
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
|
||||
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
|
||||
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
|
||||
|
||||
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
|
||||
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
|
||||
@@ -130,4 +131,110 @@ class AccountController extends Controller
|
||||
session()->flash('message-type', 'success');
|
||||
return redirect()->route('index');
|
||||
}
|
||||
|
||||
public static function getTFAInstance()
|
||||
{
|
||||
$tfa = new TwoFactorAuth(new QRCodeProvider(), 'STEMMechanics', 6, 30, Algorithm::Sha512);
|
||||
$tfa->ensureCorrectTime();
|
||||
return $tfa;
|
||||
}
|
||||
|
||||
public function show_tfa()
|
||||
{
|
||||
$user = auth()->user();
|
||||
if ($user->tfa_secret === null) {
|
||||
$tfa = self::getTFAInstance();
|
||||
$secret = $tfa->createSecret();
|
||||
|
||||
return response()->json([
|
||||
'secret' => $secret,
|
||||
]);
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function show_tfa_image(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
if ($user->tfa_secret === null && $request->has('secret')) {
|
||||
$tfa = self::getTFAInstance();
|
||||
|
||||
$qrCodeProvider = new QRCodeProvider();
|
||||
$qrCode = $qrCodeProvider->getQRCodeImage(
|
||||
$tfa->getQRText($user->email, $request->get('secret')),
|
||||
200
|
||||
);
|
||||
|
||||
return response()->stream(function () use ($qrCode) {
|
||||
echo $qrCode;
|
||||
}, 200, ['Content-Type' => $qrCodeProvider->getMimeType()]);
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function post_tfa(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->tfa_secret === null && $request->has('secret') && $request->has('code')) {
|
||||
$secret = $request->get('secret');
|
||||
$code = $request->get('code');
|
||||
|
||||
$tfa = self::getTFAInstance();
|
||||
|
||||
if ($tfa->verifyCode($secret, $code, 4)) {
|
||||
$user->tfa_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$codes = $user->generateBackupCodes();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'codes' => $codes
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy_tfa(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->tfa_secret !== null) {
|
||||
$user->tfa_secret = null;
|
||||
$user->save();
|
||||
|
||||
$user->backupCodes()->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
]);
|
||||
} else {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function post_tfa_reset_backup_codes(Request $request)
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if ($user->tfa_secret !== null) {
|
||||
$codes = $user->generateBackupCodes();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'codes' => $codes
|
||||
]);
|
||||
} else {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
use App\Jobs\SendEmail;
|
||||
use App\Mail\UserEmailUpdateConfirm;
|
||||
use App\Mail\UserLogin;
|
||||
use App\Mail\UserLoginBackupCode;
|
||||
use App\Mail\UserRegister;
|
||||
use App\Mail\UserWelcome;
|
||||
use App\Models\Token;
|
||||
@@ -47,13 +48,60 @@ class AuthController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'captcha' => 'required_captcha',
|
||||
], [
|
||||
'email.required' => __('validation.custom_messages.email_required'),
|
||||
'email.email' => __('validation.custom_messages.email_invalid'),
|
||||
]);
|
||||
|
||||
$forceEmailLogin = false;
|
||||
|
||||
if($request->has('code')) {
|
||||
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
|
||||
if($user) {
|
||||
$tfa = AccountController::getTFAInstance();
|
||||
if ($request->code && $tfa->verifyCode($user->tfa_secret, $request->code, 4)) {
|
||||
$data = ['url' => session()->pull('url.intended', null)];
|
||||
return $this->loginByUser($user, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return view('auth.login-2fa', ['email' => $request->email])->withErrors([
|
||||
'code' => 'The 2FA code is not valid',
|
||||
]);
|
||||
}
|
||||
|
||||
if($request->has('backup_code')) {
|
||||
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
|
||||
if($user) {
|
||||
if($user->verifyBackupCode($request->backup_code)) {
|
||||
$data = ['url' => session()->pull('url.intended', null)];
|
||||
|
||||
dispatch(new SendEmail($user->email, new UserLoginBackupCode($user->email)))->onQueue('mail');
|
||||
|
||||
return $this->loginByUser($user, $data);
|
||||
}
|
||||
}
|
||||
|
||||
return view('auth.login-2fa', ['email' => $request->email, 'method' => 'backup'])->withErrors([
|
||||
'backup_code' => 'The backup code is not valid',
|
||||
]);
|
||||
}
|
||||
|
||||
if($request->has('method')) {
|
||||
if($request->get('method') === 'email') {
|
||||
$forceEmailLogin = true;
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
|
||||
if($user) {
|
||||
if ($user) {
|
||||
if (!$forceEmailLogin && $user->tfa_secret !== null) {
|
||||
return view('auth.login-2fa', ['user' => $user]);
|
||||
}
|
||||
|
||||
$token = $user->tokens()->create([
|
||||
'type' => 'login',
|
||||
'data' => ['url' => session()->pull('url.intended', null)],
|
||||
@@ -190,6 +238,7 @@ class AuthController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'captcha' => 'required_captcha',
|
||||
], [
|
||||
'email.required' => __('validation.custom_messages.email_required'),
|
||||
'email.email' => __('validation.custom_messages.email_invalid')
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Event;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EventController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$homeView = true;
|
||||
$search = $request->get('search', '');
|
||||
|
||||
$query = Event::query();
|
||||
|
||||
if(!auth()->user()?->admin) {
|
||||
$query = $query->where('status', '!=', 'draft');
|
||||
}
|
||||
|
||||
if($request->has('search') && $request->search !== '') {
|
||||
$homeView = false;
|
||||
$query = $query->where(function ($query) use ($request) {
|
||||
$query->where('title', 'like', '%' . $request->search . '%')
|
||||
->orWhere('content', 'like', '%' . $request->search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if($request->has('location') && $request->location !== '') {
|
||||
$homeView = false;
|
||||
$query = $query->whereHas('location', function ($query) use ($request) {
|
||||
$query->where('name', 'like', '%' . $request->location . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if($request->has('date') && $request->date !== '') {
|
||||
$homeView = false;
|
||||
$dates = explode('-', $request->date);
|
||||
$dates = array_map('trim', $dates);
|
||||
$dates = array_map(function($date) {
|
||||
$date = trim($date);
|
||||
|
||||
if(preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
return $date;
|
||||
}
|
||||
if(preg_match('/^(\d{2})-(\d{2})-(\d{2})$/', $date, $matches)) {
|
||||
return '20' . $matches[1] . '-' . $matches[2] . '-' . $matches[3];
|
||||
}
|
||||
if(preg_match('/^\d{4}-\d{2}$/', $date)) {
|
||||
return $date . '-01';
|
||||
}
|
||||
if(preg_match('/^\d{4}$/', $date)) {
|
||||
return $date . '-01-01';
|
||||
}
|
||||
if(preg_match('/^(\d{2})\/(\d{2})\/(\d{2})$/', $date, $matches)) {
|
||||
return '20' . $matches[3] . '-' . $matches[2] . '-' . $matches[1];
|
||||
}
|
||||
if(preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $date, $matches)) {
|
||||
return $matches[3] . '-' . $matches[2] . '-' . $matches[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}, $dates);
|
||||
|
||||
if(count($dates) == 2) {
|
||||
// If there are two dates, filter between starts_at and ends_at
|
||||
$query = $query->whereDate('starts_at', '>=', $dates[0])
|
||||
->whereDate('ends_at', '<=', $dates[1]);
|
||||
} else {
|
||||
// If there is one date, filter starts_at that date or newer
|
||||
$query = $query->whereDate('starts_at', '>=', $dates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if($homeView) {
|
||||
$query = $query->where('starts_at', '>=', Carbon::now()->subDays(8))
|
||||
->orderBy('starts_at', 'asc');
|
||||
} else {
|
||||
$query = $query->orderBy('starts_at', 'asc');
|
||||
}
|
||||
|
||||
$events = $query->paginate(12);
|
||||
return view('event.index', [
|
||||
'events' => $events,
|
||||
'search' => $search,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function admin_index(Request $request)
|
||||
{
|
||||
$query = Event::query();
|
||||
|
||||
if($request->has('search')) {
|
||||
$query->where('title', 'like', '%' . $request->search . '%');
|
||||
$query->orWhere('content', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
$events = $query->orderBy('starts_at', 'desc')->paginate(12)->onEachSide(1);
|
||||
|
||||
return view('admin.event.index', [
|
||||
'events' => $events
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function admin_create()
|
||||
{
|
||||
return view('admin.event.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function admin_store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required',
|
||||
'content' => 'required',
|
||||
'starts_at' => 'required',
|
||||
'ends_at' => 'required|after:starts_at',
|
||||
'publish_at' => 'required',
|
||||
'closes_at' => 'required',
|
||||
'status' => 'required',
|
||||
'hero_media_name' => 'required|exists:media,name',
|
||||
'registration_data' => 'required_unless:registration,none',
|
||||
], [
|
||||
'title.required' => __('validation.custom_messages.title_required'),
|
||||
'content.required' => __('validation.custom_messages.content_required'),
|
||||
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
|
||||
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
|
||||
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
|
||||
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
|
||||
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
|
||||
'status.required' => __('validation.custom_messages.status_required'),
|
||||
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
|
||||
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
|
||||
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
|
||||
]);
|
||||
|
||||
$eventData = $request->all();
|
||||
$eventData['user_id'] = auth()->user()->id;
|
||||
|
||||
if($eventData['status'] === 'open' && Carbon::parse($eventData['starts_at'])->lt(Carbon::now())) {
|
||||
$eventData['status'] = 'closed';
|
||||
}
|
||||
|
||||
$event = Event::create($eventData);
|
||||
$event->updateFiles($request->input('files'));
|
||||
|
||||
session()->flash('message', 'Event has been created');
|
||||
session()->flash('message-title', 'Event created');
|
||||
session()->flash('message-type', 'success');
|
||||
return redirect()->route('admin.event.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Event $event)
|
||||
{
|
||||
if(!auth()->user()?->admin && $event->status == 'draft') {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('event.show', ['event' => $event]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function admin_edit(Event $event)
|
||||
{
|
||||
return view('admin.event.edit', ['event' => $event]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function admin_update(Request $request, Event $event)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required',
|
||||
'content' => 'required',
|
||||
'starts_at' => 'required',
|
||||
'ends_at' => 'required|after:starts_at',
|
||||
'publish_at' => 'required',
|
||||
'closes_at' => 'required',
|
||||
'status' => 'required',
|
||||
'hero_media_name' => 'required|exists:media,name',
|
||||
'registration_data' => 'required_unless:registration,none',
|
||||
], [
|
||||
'title.required' => __('validation.custom_messages.title_required'),
|
||||
'content.required' => __('validation.custom_messages.content_required'),
|
||||
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
|
||||
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
|
||||
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
|
||||
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
|
||||
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
|
||||
'status.required' => __('validation.custom_messages.status_required'),
|
||||
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
|
||||
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
|
||||
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
|
||||
]);
|
||||
|
||||
$eventData = $request->all();
|
||||
if($eventData['status'] === 'open' && Carbon::parse($eventData['starts_at'])->lt(Carbon::now())) {
|
||||
$eventData['status'] = 'closed';
|
||||
}
|
||||
|
||||
$event->update($eventData);
|
||||
$event->updateFiles($request->input('files'));
|
||||
|
||||
session()->flash('message', 'Event has been updated');
|
||||
session()->flash('message-title', 'Event updated');
|
||||
session()->flash('message-type', 'success');
|
||||
return redirect()->route('admin.event.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function admin_destroy(Event $event)
|
||||
{
|
||||
$event->delete();
|
||||
session()->flash('message', 'Event has been deleted');
|
||||
session()->flash('message-title', 'Event deleted');
|
||||
session()->flash('message-type', 'danger');
|
||||
|
||||
return redirect()->route('admin.event.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate the specified resource.
|
||||
*/
|
||||
public function admin_duplicate(Event $event)
|
||||
{
|
||||
$newWorkshop = $event->replicate();
|
||||
$newWorkshop->title = $newWorkshop->title . ' (copy)';
|
||||
$newWorkshop->status = 'draft';
|
||||
$newWorkshop->save();
|
||||
|
||||
foreach($event->files as $file) {
|
||||
$newWorkshop->files()->attach($file->name);
|
||||
}
|
||||
|
||||
session()->flash('message', 'Event has been duplicated');
|
||||
session()->flash('message-title', 'Event duplicated');
|
||||
session()->flash('message-type', 'success');
|
||||
|
||||
return redirect()->route('admin.event.edit', $newWorkshop);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,18 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\Event;
|
||||
use App\Models\Workshop;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$posts = Post::query()->orderBy('created_at', 'desc')->limit(4)->get();
|
||||
$events = Event::query()->where('starts_at', '>', now())->where('status', '!=', 'private')->orderBy('starts_at', 'asc')->limit(4)->get();
|
||||
$workshops = Workshop::query()->where('starts_at', '>', now())->where('status', '!=', 'private')->orderBy('starts_at', 'asc')->limit(4)->get();
|
||||
|
||||
return view('home', [
|
||||
'posts' => $posts,
|
||||
'events' => $events,
|
||||
'workshops' => $workshops,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
57
app/Http/Controllers/SearchController.php
Normal file
57
app/Http/Controllers/SearchController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\Workshop;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$search = $request->get('q', '');
|
||||
$search_words = explode(' ', $search); // Split the search query into words[1]
|
||||
|
||||
$workshopQuery = Workshop::query()->where('status', '!=', 'draft');
|
||||
|
||||
$workshopQuery->where(function ($query) use ($search_words) {
|
||||
foreach ($search_words as $word) {
|
||||
$query->orWhere(function ($subQuery) use ($word) {
|
||||
$subQuery->where('title', 'like', '%' . $word . '%')
|
||||
->orWhere('content', 'like', '%' . $word . '%')
|
||||
->orWhereHas('location', function ($locationQuery) use ($word) {
|
||||
$locationQuery->where('name', 'like', '%' . $word . '%');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$workshops = $workshopQuery->orderBy('starts_at', 'desc')
|
||||
->paginate(6, ['*'], 'workshop');
|
||||
|
||||
$postQuery = Post::query()->where('status', 'published');
|
||||
$postQuery->where(function ($query) use ($search_words) {
|
||||
foreach ($search_words as $word) {
|
||||
$query->where(function ($subQuery) use ($word) {
|
||||
$subQuery->where('title', 'like', '%' . $word . '%')
|
||||
->orWhere('content', 'like', '%' . $word . '%');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$posts = $postQuery->orderBy('created_at', 'desc')
|
||||
->paginate(6, ['*'], 'post')
|
||||
->onEachSide(1);
|
||||
|
||||
return view('search', [
|
||||
'workshops' => $workshops,
|
||||
'posts' => $posts,
|
||||
'search' => $search,
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/SubscribeController.php
Normal file
44
app/Http/Controllers/SubscribeController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EmailSubscriptions;
|
||||
use App\Models\SentEmail;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SubscribeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function destroy($email)
|
||||
{
|
||||
$emailModel = SentEmail::where('id', $email)->first();
|
||||
|
||||
if (!$emailModel) {
|
||||
// Email not found, redirect to home page with a message
|
||||
return redirect()->route('index')->with([
|
||||
'message' => 'The unsubscribe link is invalid or has expired.',
|
||||
'message-title' => 'Invalid Unsubscribe Link',
|
||||
'message-type' => 'warning'
|
||||
]);
|
||||
}
|
||||
|
||||
// Existing unsubscribe logic
|
||||
$subscriptions = EmailSubscriptions::where('email', $emailModel->recipient)->get();
|
||||
|
||||
if ($subscriptions->isEmpty()) {
|
||||
session()->flash('message', 'You are already unsubscribed.');
|
||||
session()->flash('message-title', 'Already Unsubscribed');
|
||||
session()->flash('message-type', 'info');
|
||||
} else {
|
||||
EmailSubscriptions::where('email', $emailModel->recipient)->delete();
|
||||
|
||||
session()->flash('message', 'You have been successfully unsubscribed.');
|
||||
session()->flash('message-title', 'Unsubscribed');
|
||||
session()->flash('message-type', 'success');
|
||||
}
|
||||
|
||||
return redirect()->route('index');
|
||||
}
|
||||
}
|
||||
@@ -49,11 +49,11 @@ class UserController extends Controller
|
||||
'email' => 'email|unique:users',
|
||||
'phone' => '',
|
||||
|
||||
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
|
||||
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
|
||||
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
|
||||
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
|
||||
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
|
||||
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
|
||||
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
|
||||
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
|
||||
|
||||
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
|
||||
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
|
||||
@@ -67,11 +67,11 @@ class UserController extends Controller
|
||||
'email.email' => __('validation.custom_messages.email_invalid'),
|
||||
'phone.required' => __('validation.custom_messages.phone_required'),
|
||||
|
||||
'home_address.required' => __('validation.custom_messages.home_address_required'),
|
||||
'home_city.required' => __('validation.custom_messages.home_city_required'),
|
||||
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
|
||||
'home_country.required' => __('validation.custom_messages.home_country_required'),
|
||||
'home_state.required' => __('validation.custom_messages.home_state_required'),
|
||||
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
|
||||
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
|
||||
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
|
||||
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
|
||||
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
|
||||
|
||||
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
|
||||
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
|
||||
@@ -107,11 +107,11 @@ class UserController extends Controller
|
||||
'email' => ['email', Rule::unique('users')->ignore($user->id)],
|
||||
'phone' => '',
|
||||
|
||||
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
|
||||
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
|
||||
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
|
||||
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
|
||||
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
|
||||
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
|
||||
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
|
||||
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
|
||||
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
|
||||
|
||||
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
|
||||
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
|
||||
@@ -125,11 +125,11 @@ class UserController extends Controller
|
||||
'email.email' => __('validation.custom_messages.email_invalid'),
|
||||
'phone.required' => __('validation.custom_messages.phone_required'),
|
||||
|
||||
'home_address.required' => __('validation.custom_messages.home_address_required'),
|
||||
'home_city.required' => __('validation.custom_messages.home_city_required'),
|
||||
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
|
||||
'home_country.required' => __('validation.custom_messages.home_country_required'),
|
||||
'home_state.required' => __('validation.custom_messages.home_state_required'),
|
||||
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
|
||||
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
|
||||
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
|
||||
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
|
||||
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
|
||||
|
||||
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
|
||||
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
|
||||
|
||||
209
app/Http/Controllers/WorkshopController.php
Normal file
209
app/Http/Controllers/WorkshopController.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Workshop;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class WorkshopController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$query = Workshop::query();
|
||||
$query = $query->where('starts_at', '>=', Carbon::now()->subDays(8))
|
||||
->orderBy('starts_at', 'asc');
|
||||
|
||||
$workshops = $query->paginate(12);
|
||||
return view('workshop.index', [
|
||||
'workshops' => $workshops
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function past_index()
|
||||
{
|
||||
$query = Workshop::query();
|
||||
$query = $query->where('starts_at', '<', Carbon::now())
|
||||
->orderBy('starts_at', 'desc');
|
||||
|
||||
$workshops = $query->paginate(12);
|
||||
return view('workshop.index', [
|
||||
'workshops' => $workshops
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function admin_index(Request $request)
|
||||
{
|
||||
$query = Workshop::query();
|
||||
|
||||
if($request->has('search')) {
|
||||
$query->where('title', 'like', '%' . $request->search . '%');
|
||||
$query->orWhere('content', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
$workshops = $query->orderBy('starts_at', 'desc')->paginate(12)->onEachSide(1);
|
||||
|
||||
return view('admin.workshop.index', [
|
||||
'workshops' => $workshops
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function admin_create()
|
||||
{
|
||||
return view('admin.workshop.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function admin_store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required',
|
||||
'content' => 'required',
|
||||
'starts_at' => 'required',
|
||||
'ends_at' => 'required|after:starts_at',
|
||||
'publish_at' => 'required',
|
||||
'closes_at' => 'required',
|
||||
'status' => 'required',
|
||||
'hero_media_name' => 'required|exists:media,name',
|
||||
'registration_data' => 'required_unless:registration,none',
|
||||
], [
|
||||
'title.required' => __('validation.custom_messages.title_required'),
|
||||
'content.required' => __('validation.custom_messages.content_required'),
|
||||
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
|
||||
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
|
||||
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
|
||||
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
|
||||
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
|
||||
'status.required' => __('validation.custom_messages.status_required'),
|
||||
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
|
||||
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
|
||||
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
|
||||
]);
|
||||
|
||||
$workshopData = $request->all();
|
||||
$workshopData['user_id'] = auth()->user()->id;
|
||||
|
||||
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
|
||||
$workshopData['status'] = 'closed';
|
||||
}
|
||||
|
||||
$workshop = Workshop::create($workshopData);
|
||||
$workshop->updateFiles($request->input('files'));
|
||||
|
||||
session()->flash('message', 'Workshop has been created');
|
||||
session()->flash('message-title', 'Workshop created');
|
||||
session()->flash('message-type', 'success');
|
||||
return redirect()->route('admin.workshop.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Workshop $workshop)
|
||||
{
|
||||
if(!auth()->user()?->admin && $workshop->status == 'draft') {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('workshop.show', ['workshop' => $workshop]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function admin_edit(Workshop $workshop)
|
||||
{
|
||||
return view('admin.workshop.edit', ['workshop' => $workshop]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function admin_update(Request $request, Workshop $workshop)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required',
|
||||
'content' => 'required',
|
||||
'starts_at' => 'required',
|
||||
'ends_at' => 'required|after:starts_at',
|
||||
'publish_at' => 'required',
|
||||
'closes_at' => 'required',
|
||||
'status' => 'required',
|
||||
'hero_media_name' => 'required|exists:media,name',
|
||||
'registration_data' => 'required_unless:registration,none',
|
||||
], [
|
||||
'title.required' => __('validation.custom_messages.title_required'),
|
||||
'content.required' => __('validation.custom_messages.content_required'),
|
||||
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
|
||||
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
|
||||
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
|
||||
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
|
||||
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
|
||||
'status.required' => __('validation.custom_messages.status_required'),
|
||||
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
|
||||
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
|
||||
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
|
||||
]);
|
||||
|
||||
$workshopData = $request->all();
|
||||
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
|
||||
$workshopData['status'] = 'closed';
|
||||
}
|
||||
|
||||
$workshop->update($workshopData);
|
||||
$workshop->updateFiles($request->input('files'));
|
||||
|
||||
session()->flash('message', 'Workshop has been updated');
|
||||
session()->flash('message-title', 'Workshop updated');
|
||||
session()->flash('message-type', 'success');
|
||||
return redirect()->route('admin.workshop.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function admin_destroy(Workshop $workshop)
|
||||
{
|
||||
$workshop->delete();
|
||||
session()->flash('message', 'Workshop has been deleted');
|
||||
session()->flash('message-title', 'Workshop deleted');
|
||||
session()->flash('message-type', 'danger');
|
||||
|
||||
return redirect()->route('admin.workshop.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate the specified resource.
|
||||
*/
|
||||
public function admin_duplicate(Workshop $workshop)
|
||||
{
|
||||
$newWorkshop = $workshop->replicate();
|
||||
$newWorkshop->title = $newWorkshop->title . ' (copy)';
|
||||
$newWorkshop->status = 'draft';
|
||||
$newWorkshop->save();
|
||||
|
||||
foreach($workshop->files as $file) {
|
||||
$newWorkshop->files()->attach($file->name);
|
||||
}
|
||||
|
||||
session()->flash('message', 'Workshop has been duplicated');
|
||||
session()->flash('message-title', 'Workshop duplicated');
|
||||
session()->flash('message-type', 'success');
|
||||
|
||||
return redirect()->route('admin.workshop.edit', $newWorkshop);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\SentEmail;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendEmail implements ShouldQueue
|
||||
@@ -48,6 +50,18 @@ class SendEmail implements ShouldQueue
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
// Record sent email
|
||||
$sentEmail = SentEmail::create([
|
||||
'recipient' => $this->to,
|
||||
'mailable_class' => get_class($this->mailable)
|
||||
]);
|
||||
|
||||
// Add unsubscribe link if mailable supports it
|
||||
if (method_exists($this->mailable, 'withUnsubscribeLink')) {
|
||||
$unsubscribeLink = route('unsubscribe', ['email' => $sentEmail->id]);
|
||||
$this->mailable->withUnsubscribeLink($unsubscribeLink);
|
||||
}
|
||||
|
||||
Mail::to($this->to)->send($this->mailable);
|
||||
}
|
||||
}
|
||||
|
||||
104
app/Livewire/EmailSubscribe.php
Normal file
104
app/Livewire/EmailSubscribe.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\SendEmail;
|
||||
use Carbon\Carbon;
|
||||
use Livewire\Component;
|
||||
use App\Models\EmailSubscriptions;
|
||||
use App\Mail\UserWelcome;
|
||||
|
||||
class EmailSubscribe extends Component
|
||||
{
|
||||
public string $email = '';
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
public string $trap = '';
|
||||
public int $renderedAt; // unix timestamp
|
||||
|
||||
protected $rules = [
|
||||
'email' => 'required|email|max:255',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->renderedAt = now()->timestamp;
|
||||
}
|
||||
|
||||
public function subscribe(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
// 1. Honeypot - if this hidden field is filled, treat as success but do nothing
|
||||
if (! empty($this->trap)) {
|
||||
$this->reset(['email', 'trap']);
|
||||
$this->success = true;
|
||||
$this->message = 'Thanks, you have been subscribed to our newsletter.';
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Block submits in first 10 seconds after render
|
||||
if (now()->timestamp - $this->renderedAt < 4) {
|
||||
$this->success = false;
|
||||
$this->message = 'That was a bit quick. Please wait a few seconds and try again.';
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Enforce 30 seconds between attempts per session
|
||||
$lastAttempt = session('subscribe_last_attempt'); // int timestamp or null
|
||||
if (! is_int($lastAttempt)) {
|
||||
$lastAttempt = null;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
if ($lastAttempt && ($now - $lastAttempt) < 20) {
|
||||
$this->success = false;
|
||||
$this->message = 'Please wait a little before trying again.';
|
||||
return;
|
||||
}
|
||||
|
||||
session(['subscribe_last_attempt' => $now]);
|
||||
|
||||
// 4. Limit to 5 attempts per session (your existing logic)
|
||||
$attempts = session('subscribe_attempts', 0);
|
||||
if ($attempts >= 5) {
|
||||
$this->success = false;
|
||||
$this->message = 'Too many attempts. Please try again in a little while.';
|
||||
return;
|
||||
}
|
||||
session(['subscribe_attempts' => $attempts + 1]);
|
||||
|
||||
// Look up existing subscription by email
|
||||
$subscription = EmailSubscriptions::where('email', $this->email)->first();
|
||||
|
||||
// If already confirmed, do not create a new record or resend confirmation
|
||||
if ($subscription && $subscription->confirmed) {
|
||||
// Optionally you could set a different flag or message here
|
||||
$this->success = false;
|
||||
$this->message = 'That email is already subscribed to our newsletter.';
|
||||
} else {
|
||||
// If no subscription exists, create a new unconfirmed one
|
||||
if (!$subscription) {
|
||||
$subscription = EmailSubscriptions::create([
|
||||
'email' => $this->email,
|
||||
'confirmed' => Carbon::now()
|
||||
]);
|
||||
|
||||
$subscription->save();
|
||||
}
|
||||
|
||||
dispatch(new SendEmail($subscription->email, new UserWelcome($subscription->email)))->onQueue('mail');
|
||||
|
||||
$this->success = true;
|
||||
$this->message = 'Thanks, you have been subscribed to our newsletter.';
|
||||
}
|
||||
|
||||
$this->reset(['email', 'trap']);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.email-subscribe');
|
||||
}
|
||||
}
|
||||
58
app/Mail/UpcomingWorkshops.php
Normal file
58
app/Mail/UpcomingWorkshops.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Workshop;
|
||||
use App\Traits\HasUnsubscribeLink;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class UpcomingWorkshops extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, HasUnsubscribeLink;
|
||||
|
||||
public $subject;
|
||||
public $email;
|
||||
public $workshops;
|
||||
|
||||
public function __construct($email, $subject = 'Upcoming Workshops 🌟')
|
||||
{
|
||||
$this->subject = $subject;
|
||||
$this->email = $email;
|
||||
$this->workshops = $this->getUpcomingWorkshops();
|
||||
}
|
||||
|
||||
private function getUpcomingWorkshops()
|
||||
{
|
||||
$startDate = Carbon::now()->addDays(3);
|
||||
$endDate = Carbon::now()->addDays(42);
|
||||
|
||||
return Workshop::select('workshops.*', 'locations.name as location_name')
|
||||
->join('locations', 'workshops.location_id', '=', 'locations.id')
|
||||
->whereIn('workshops.status', ['open','scheduled'])
|
||||
->whereBetween('workshops.starts_at', [$startDate, $endDate])
|
||||
->where('locations.name', 'not like', '%private%')
|
||||
->orderBy('locations.name')
|
||||
->orderBy('workshops.starts_at')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
// Bail if there are no upcoming workshops
|
||||
if ($this->workshops->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this
|
||||
->subject($this->subject)
|
||||
->markdown('emails.upcoming-workshops')
|
||||
->with([
|
||||
'email' => $this->email,
|
||||
'workshops' => $this->workshops,
|
||||
'unsubscribeLink' => $this->unsubscribeLink
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Mail/UserLoginBackupCode.php
Normal file
31
app/Mail/UserLoginBackupCode.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Spatie\LaravelPdf\Facades\Pdf;
|
||||
|
||||
class UserLoginBackupCode extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $email;
|
||||
public function __construct($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->subject('Hey, did you recently log in?')
|
||||
->markdown('emails.login-backup-code')
|
||||
->with([
|
||||
'email' => $this->email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Mail/UserLoginTFADisabled.php
Normal file
31
app/Mail/UserLoginTFADisabled.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Spatie\LaravelPdf\Facades\Pdf;
|
||||
|
||||
class UserLoginTFADisabled extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $email;
|
||||
public function __construct($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->subject('Two-factor authentication disabled on your account')
|
||||
->markdown('emails.login-tfa-disabled')
|
||||
->with([
|
||||
'email' => $this->email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Mail/UserLoginTFAEnabled.php
Normal file
31
app/Mail/UserLoginTFAEnabled.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Spatie\LaravelPdf\Facades\Pdf;
|
||||
|
||||
class UserLoginTFAEnabled extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $email;
|
||||
public function __construct($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
return $this
|
||||
->subject('Two-factor authentication enabled on your account')
|
||||
->markdown('emails.login-tfa-enabled')
|
||||
->with([
|
||||
'email' => $this->email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Traits\HasUnsubscribeLink;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserWelcome extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
use Queueable, SerializesModels, HasUnsubscribeLink;
|
||||
|
||||
public $email;
|
||||
|
||||
@@ -25,6 +25,7 @@ class UserWelcome extends Mailable
|
||||
->markdown('emails.welcome')
|
||||
->with([
|
||||
'email' => $this->email,
|
||||
'unsubscribeLink' => $this->unsubscribeLink
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailSubscriptions extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
|
||||
30
app/Models/SentEmail.php
Normal file
30
app/Models/SentEmail.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SentEmail extends Model
|
||||
{
|
||||
protected $fillable = ['recipient', 'mailable_class'];
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
/**
|
||||
* Boot function from Laravel.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->{$model->getKeyName()}) === true) {
|
||||
$model->{$model->getKeyName()} = strtolower(Str::random(15));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\SendEmail;
|
||||
use App\Mail\UserLoginTFADisabled;
|
||||
use App\Mail\UserLoginTFAEnabled;
|
||||
use App\Traits\UUID;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
@@ -24,19 +28,21 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'surname',
|
||||
'email',
|
||||
'phone',
|
||||
'home_address',
|
||||
'home_address2',
|
||||
'home_city',
|
||||
'home_postcode',
|
||||
'home_state',
|
||||
'home_country',
|
||||
'shipping_address',
|
||||
'shipping_address2',
|
||||
'shipping_city',
|
||||
'shipping_postcode',
|
||||
'shipping_state',
|
||||
'shipping_country',
|
||||
'billing_address',
|
||||
'billing_address2',
|
||||
'billing_city',
|
||||
'billing_postcode',
|
||||
'billing_state',
|
||||
'billing_country',
|
||||
'subscribed'
|
||||
'subscribed',
|
||||
'tfa_secret',
|
||||
'agree_tos',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -47,6 +53,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'tfa_secret'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -98,6 +105,15 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($user->isDirty('tfa_secret')) {
|
||||
if($user->tfa_secret === null) {
|
||||
$user->backupCodes()->delete();
|
||||
dispatch(new SendEmail($user->email, new UserLoginTFADisabled($user->email)))->onQueue('mail');
|
||||
} else {
|
||||
dispatch(new SendEmail($user->email, new UserLoginTFAEnabled($user->email)))->onQueue('mail');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::deleting(function ($user) {
|
||||
@@ -176,4 +192,38 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
return $this->admin === 1;
|
||||
}
|
||||
|
||||
public function backupCodes()
|
||||
{
|
||||
return $this->hasMany(UserBackupCode::class);
|
||||
}
|
||||
|
||||
public function generateBackupCodes()
|
||||
{
|
||||
$this->backupCodes()->delete();
|
||||
$codes = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$code = strtoupper(bin2hex(random_bytes(4)));
|
||||
$codes[] = $code;
|
||||
|
||||
UserBackupCode::create([
|
||||
'user_id' => $this->id,
|
||||
'code' => $code,
|
||||
]);
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
public function verifyBackupCode($code)
|
||||
{
|
||||
$backupCodes = $this->backupCodes()->get();
|
||||
foreach ($backupCodes as $backupCode) {
|
||||
if (Hash::check($code, $backupCode->code)) {
|
||||
$backupCode->delete();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
44
app/Models/UserBackupCode.php
Normal file
44
app/Models/UserBackupCode.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserBackupCode extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'code'
|
||||
];
|
||||
|
||||
/**
|
||||
* Set the code attribute and automatically hash the code.
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function setCodeAttribute($value)
|
||||
{
|
||||
$this->attributes['code'] = Hash::make($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given code against the stored hashed code.
|
||||
*
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
public function verify($value)
|
||||
{
|
||||
return Hash::check($value, $this->code);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use App\Traits\Slug;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Event extends Model
|
||||
class Workshop extends Model
|
||||
{
|
||||
use HasFactory, Slug, HasFiles;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -21,6 +21,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->environment('production')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
Blade::directive('includeSVG', function ($arguments) {
|
||||
list($path, $styles) = array_pad(explode(',', str_replace(['(', ')', ' ', "'"], '', $arguments), 2), 2, '');
|
||||
$svgContent = file_get_contents(public_path($path));
|
||||
|
||||
62
app/Providers/CaptchaServiceProvider.php
Normal file
62
app/Providers/CaptchaServiceProvider.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class CaptchaServiceProvider extends ServiceProvider
|
||||
{
|
||||
private string $captchaKey = '6Lc6BIAUAAAAAABZzv6J9ZQ7J9Zzv6J9ZQ7J9Zzv';
|
||||
private int $timeThreshold = 750;
|
||||
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Blade::directive('captcha', function () {
|
||||
return <<<EOT
|
||||
<input type="text" name="captcha" autocomplete="off" style="position:absolute;left:-9999px;top:-9999px">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const errors = {!! json_encode(\$errors->getMessages()) !!};
|
||||
if(errors && errors.captcha && errors.captcha.length) {
|
||||
SM.alert('', errors.captcha[0], 'danger');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
EOT;
|
||||
});
|
||||
|
||||
Blade::directive('captchaScripts', function () {
|
||||
return <<<EOT
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.setTimeout(function() {
|
||||
const captchaList = document.querySelectorAll('input[name="captcha"]');
|
||||
captchaList.forEach(function(captcha) {
|
||||
if(captcha.value === '') {
|
||||
captcha.value = '$this->captchaKey';
|
||||
}
|
||||
});
|
||||
}, $this->timeThreshold);
|
||||
});
|
||||
</script>
|
||||
EOT;
|
||||
});
|
||||
|
||||
Validator::extend('required_captcha', function ($attribute, $value, $parameters, $validator) {
|
||||
return $value === $this->captchaKey;
|
||||
}, 'The form captcha failed to validate. Please try again.');
|
||||
}
|
||||
}
|
||||
23
app/Providers/QRCodeProvider.php
Normal file
23
app/Providers/QRCodeProvider.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
|
||||
class QRCodeProvider implements IQRCodeProvider
|
||||
{
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
|
||||
public function getQRCodeImage(string $qrText, int $size): string
|
||||
{
|
||||
$options = new QROptions;
|
||||
$options->outputBase64 = false;
|
||||
$options->imageTransparent = true;
|
||||
return (new QRCode($options))->render($qrText);
|
||||
}
|
||||
}
|
||||
14
app/Traits/HasUnsubscribeLink.php
Normal file
14
app/Traits/HasUnsubscribeLink.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
trait HasUnsubscribeLink
|
||||
{
|
||||
protected $unsubscribeLink;
|
||||
|
||||
public function withUnsubscribeLink($link)
|
||||
{
|
||||
$this->unsubscribeLink = $link;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ use Illuminate\Support\Str;
|
||||
|
||||
trait Slug
|
||||
{
|
||||
protected $appendsSlug = ['slug'];
|
||||
|
||||
/**
|
||||
* Boot function from Laravel.
|
||||
*
|
||||
@@ -20,6 +22,16 @@ trait Slug
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the trait.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function initializeSlug(): void
|
||||
{
|
||||
$this->appends = array_merge($this->appends ?? [], $this->appendsSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value indicating whether the IDs are incrementing.
|
||||
*
|
||||
@@ -47,7 +59,7 @@ trait Slug
|
||||
*/
|
||||
public function getRouteKey()
|
||||
{
|
||||
return $this->slug();
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +80,7 @@ trait Slug
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function slug()
|
||||
public function getSlugAttribute()
|
||||
{
|
||||
return Str::slug($this->title) . '-' . $this->id;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\CaptchaServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-imagick": "*",
|
||||
"chillerlan/php-qrcode": "^5.0",
|
||||
"gehrisandro/tailwind-merge-laravel": "^1.2",
|
||||
"intervention/image": "^3.5",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.4",
|
||||
"php-ffmpeg/php-ffmpeg": "^1.2",
|
||||
"ext-imagick": "*"
|
||||
"robthree/twofactorauth": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
|
||||
3682
composer.lock
generated
3682
composer.lock
generated
File diff suppressed because it is too large
Load Diff
80
config/flare.php
Normal file
80
config/flare.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
use Spatie\FlareClient\FlareMiddleware\AddGitInformation;
|
||||
use Spatie\FlareClient\FlareMiddleware\RemoveRequestIp;
|
||||
use Spatie\FlareClient\FlareMiddleware\CensorRequestBodyFields;
|
||||
use Spatie\FlareClient\FlareMiddleware\CensorRequestHeaders;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddDumps;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddEnvironmentInformation;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddExceptionInformation;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddJobs;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddLogs;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddQueries;
|
||||
use Spatie\LaravelIgnition\FlareMiddleware\AddNotifierName;
|
||||
|
||||
return [
|
||||
/*
|
||||
|
|
||||
|--------------------------------------------------------------------------
|
||||
| Flare API key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify Flare's API key below to enable error reporting to the service.
|
||||
|
|
||||
| More info: https://flareapp.io/docs/general/projects
|
||||
|
|
||||
*/
|
||||
|
||||
'key' => env('FLARE_KEY'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will modify the contents of the report sent to Flare.
|
||||
|
|
||||
*/
|
||||
|
||||
'flare_middleware' => [
|
||||
RemoveRequestIp::class,
|
||||
AddGitInformation::class,
|
||||
AddNotifierName::class,
|
||||
AddEnvironmentInformation::class,
|
||||
AddExceptionInformation::class,
|
||||
AddDumps::class,
|
||||
AddLogs::class => [
|
||||
'maximum_number_of_collected_logs' => 200,
|
||||
],
|
||||
AddQueries::class => [
|
||||
'maximum_number_of_collected_queries' => 200,
|
||||
'report_query_bindings' => true,
|
||||
],
|
||||
AddJobs::class => [
|
||||
'max_chained_job_reporting_depth' => 5,
|
||||
],
|
||||
CensorRequestBodyFields::class => [
|
||||
'censor_fields' => [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
],
|
||||
],
|
||||
CensorRequestHeaders::class => [
|
||||
'headers' => [
|
||||
'API-KEY',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reporting log statements
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If this setting is `false` log statements won't be sent as events to Flare,
|
||||
| no matter which error level you specified in the Flare log channel.
|
||||
|
|
||||
*/
|
||||
|
||||
'send_logs_as_events' => true,
|
||||
];
|
||||
277
config/ignition.php
Normal file
277
config/ignition.php
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
use Spatie\Ignition\Solutions\SolutionProviders\BadMethodCallSolutionProvider;
|
||||
use Spatie\Ignition\Solutions\SolutionProviders\MergeConflictSolutionProvider;
|
||||
use Spatie\Ignition\Solutions\SolutionProviders\UndefinedPropertySolutionProvider;
|
||||
use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder;
|
||||
use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder;
|
||||
use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder;
|
||||
use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\DefaultDbNameSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\GenericLaravelExceptionSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\IncorrectValetDbCredentialsSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\InvalidRouteActionSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingAppKeySolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingColumnSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingImportSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingLivewireComponentSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingMixManifestSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingViteManifestSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\RunningLaravelDuskInProductionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\TableNotFoundSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UndefinedViewVariableSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownValidationSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\ViewNotFoundSolutionProvider;
|
||||
use Spatie\LaravelIgnition\Solutions\SolutionProviders\OpenAiSolutionProvider;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Editor
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Choose your preferred editor to use when clicking any edit button.
|
||||
|
|
||||
| Supported: "phpstorm", "vscode", "vscode-insiders", "textmate", "emacs",
|
||||
| "sublime", "atom", "nova", "macvim", "idea", "netbeans",
|
||||
| "xdebug", "phpstorm-remote"
|
||||
|
|
||||
*/
|
||||
|
||||
'editor' => env('IGNITION_EDITOR', 'phpstorm'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Theme
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which theme Ignition should use.
|
||||
|
|
||||
| Supported: "light", "dark", "auto"
|
||||
|
|
||||
*/
|
||||
|
||||
'theme' => env('IGNITION_THEME', 'auto'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sharing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can share local errors with colleagues or others around the world.
|
||||
| Sharing is completely free and doesn't require an account on Flare.
|
||||
|
|
||||
| If necessary, you can completely disable sharing below.
|
||||
|
|
||||
*/
|
||||
|
||||
'enable_share_button' => env('IGNITION_SHARING_ENABLED', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register Ignition commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition comes with an additional make command that lets you create
|
||||
| new solution classes more easily. To keep your default Laravel
|
||||
| installation clean, this command is not registered by default.
|
||||
|
|
||||
| You can enable the command registration below.
|
||||
|
|
||||
*/
|
||||
|
||||
'register_commands' => env('REGISTER_IGNITION_COMMANDS', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Solution Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may specify a list of solution providers (as fully qualified class
|
||||
| names) that shouldn't be loaded. Ignition will ignore these classes
|
||||
| and possible solutions provided by them will never be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'solution_providers' => [
|
||||
// from spatie/ignition
|
||||
BadMethodCallSolutionProvider::class,
|
||||
MergeConflictSolutionProvider::class,
|
||||
UndefinedPropertySolutionProvider::class,
|
||||
|
||||
// from spatie/laravel-ignition
|
||||
IncorrectValetDbCredentialsSolutionProvider::class,
|
||||
MissingAppKeySolutionProvider::class,
|
||||
DefaultDbNameSolutionProvider::class,
|
||||
TableNotFoundSolutionProvider::class,
|
||||
MissingImportSolutionProvider::class,
|
||||
InvalidRouteActionSolutionProvider::class,
|
||||
ViewNotFoundSolutionProvider::class,
|
||||
RunningLaravelDuskInProductionProvider::class,
|
||||
MissingColumnSolutionProvider::class,
|
||||
UnknownValidationSolutionProvider::class,
|
||||
MissingMixManifestSolutionProvider::class,
|
||||
MissingViteManifestSolutionProvider::class,
|
||||
MissingLivewireComponentSolutionProvider::class,
|
||||
UndefinedViewVariableSolutionProvider::class,
|
||||
GenericLaravelExceptionSolutionProvider::class,
|
||||
OpenAiSolutionProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Ignored Solution Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may specify a list of solution providers (as fully qualified class
|
||||
| names) that shouldn't be loaded. Ignition will ignore these classes
|
||||
| and possible solutions provided by them will never be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'ignored_solution_providers' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Runnable Solutions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some solutions that Ignition displays are runnable and can perform
|
||||
| various tasks. By default, runnable solutions are only enabled when your
|
||||
| app has debug mode enabled and the environment is `local` or
|
||||
| `development`.
|
||||
|
|
||||
| Using the `IGNITION_ENABLE_RUNNABLE_SOLUTIONS` environment variable, you
|
||||
| can override this behaviour and enable or disable runnable solutions
|
||||
| regardless of the application's environment.
|
||||
|
|
||||
| Default: env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS')
|
||||
|
|
||||
*/
|
||||
|
||||
'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Remote Path Mapping
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you are using a remote dev server, like Laravel Homestead, Docker, or
|
||||
| even a remote VPS, it will be necessary to specify your path mapping.
|
||||
|
|
||||
| Leaving one, or both of these, empty or null will not trigger the remote
|
||||
| URL changes and Ignition will treat your editor links as local files.
|
||||
|
|
||||
| "remote_sites_path" is an absolute base path for your sites or projects
|
||||
| in Homestead, Vagrant, Docker, or another remote development server.
|
||||
|
|
||||
| Example value: "/home/vagrant/Code"
|
||||
|
|
||||
| "local_sites_path" is an absolute base path for your sites or projects
|
||||
| on your local computer where your IDE or code editor is running on.
|
||||
|
|
||||
| Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code"
|
||||
|
|
||||
*/
|
||||
|
||||
'remote_sites_path' => env('IGNITION_REMOTE_SITES_PATH', base_path()),
|
||||
'local_sites_path' => env('IGNITION_LOCAL_SITES_PATH', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Housekeeping Endpoint Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition registers a couple of routes when it is enabled. Below you may
|
||||
| specify a route prefix that will be used to host all internal links.
|
||||
|
|
||||
*/
|
||||
|
||||
'housekeeping_endpoint_prefix' => '_ignition',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings File
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition allows you to save your settings to a specific global file.
|
||||
|
|
||||
| If no path is specified, a file with settings will be saved to the user's
|
||||
| home directory. The directory depends on the OS and its settings but it's
|
||||
| typically `~/.ignition.json`. In this case, the settings will be applied
|
||||
| to all of your projects where Ignition is used and the path is not
|
||||
| specified.
|
||||
|
|
||||
| However, if you want to store your settings on a project basis, or you
|
||||
| want to keep them in another directory, you can specify a path where
|
||||
| the settings file will be saved. The path should be an existing directory
|
||||
| with correct write access.
|
||||
| For example, create a new `ignition` folder in the storage directory and
|
||||
| use `storage_path('ignition')` as the `settings_file_path`.
|
||||
|
|
||||
| Default value: '' (empty string)
|
||||
*/
|
||||
|
||||
'settings_file_path' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Recorders
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition registers a couple of recorders when it is enabled. Below you may
|
||||
| specify a recorders will be used to record specific events.
|
||||
|
|
||||
*/
|
||||
|
||||
'recorders' => [
|
||||
DumpRecorder::class,
|
||||
JobRecorder::class,
|
||||
LogRecorder::class,
|
||||
QueryRecorder::class,
|
||||
],
|
||||
|
||||
/*
|
||||
* When a key is set, we'll send your exceptions to Open AI to generate a solution
|
||||
*/
|
||||
'open_ai_key' => env('IGNITION_OPEN_AI_KEY'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include arguments
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition show you stack traces of exceptions with the arguments that were
|
||||
| passed to each method. This feature can be disabled here.
|
||||
|
|
||||
*/
|
||||
|
||||
'with_stack_frame_arguments' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Argument reducers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Ignition show you stack traces of exceptions with the arguments that were
|
||||
| passed to each method. To make these variables more readable, you can
|
||||
| specify a list of classes here which summarize the variables.
|
||||
|
|
||||
*/
|
||||
'argument_reducers' => [
|
||||
\Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer::class,
|
||||
\Spatie\LaravelIgnition\ArgumentReducers\ModelArgumentReducer::class,
|
||||
\Spatie\LaravelIgnition\ArgumentReducers\CollectionArgumentReducer::class,
|
||||
\Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer::class,
|
||||
],
|
||||
];
|
||||
50
config/tinker.php
Normal file
50
config/tinker.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Console Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to add additional Artisan commands that should
|
||||
| be available within the Tinker environment. Once the command is in
|
||||
| this array you may execute the command in Tinker using its name.
|
||||
|
|
||||
*/
|
||||
|
||||
'commands' => [
|
||||
// App\Console\Commands\ExampleCommand::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto Aliased Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Tinker will not automatically alias classes in your vendor namespaces
|
||||
| but you may explicitly allow a subset of classes to get aliased by
|
||||
| adding the names of each of those classes to the following list.
|
||||
|
|
||||
*/
|
||||
|
||||
'alias' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Classes That Should Not Be Aliased
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Typically, Tinker automatically aliases classes as you require them in
|
||||
| Tinker. However, you may wish to never alias certain classes, which
|
||||
| you may accomplish by listing the classes in the following array.
|
||||
|
|
||||
*/
|
||||
|
||||
'dont_alias' => [
|
||||
'App\Nova',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -26,11 +26,11 @@ class UserFactory extends Factory
|
||||
'email_verified_at' => now(),
|
||||
'remember_token' => Str::random(10),
|
||||
|
||||
'home_address' => fake()->streetAddress(),
|
||||
'home_city' => fake()->city(),
|
||||
'home_state' => '',
|
||||
'home_postcode' => fake()->postcode(),
|
||||
'home_country' => fake()->country(),
|
||||
'shipping_address' => fake()->streetAddress(),
|
||||
'shipping_city' => fake()->city(),
|
||||
'shipping_state' => '',
|
||||
'shipping_postcode' => fake()->postcode(),
|
||||
'shipping_country' => fake()->country(),
|
||||
|
||||
'billing_address' => fake()->streetAddress(),
|
||||
'billing_city' => fake()->city(),
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Location;
|
||||
use App\Models\Event;
|
||||
use App\Models\Workshop;
|
||||
use DateInterval;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class WorkshopFactory extends Factory
|
||||
{
|
||||
protected $model = Event::class;
|
||||
protected $model = Workshop::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<?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::rename('workshops', 'events');
|
||||
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->dropForeign(['workshop_id']);
|
||||
$table->renameColumn('workshop_id', 'event_id');
|
||||
$table->foreign('event_id')->references('id')->on('events');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::rename('events', 'workshops');
|
||||
|
||||
Schema::table('tickets', function (Blueprint $table) {
|
||||
$table->dropForeign(['event_id']);
|
||||
$table->renameColumn('event_id', 'workshop_id');
|
||||
$table->foreign('workshops_id')->references('id')->on('workshops');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('tfa_secret')->nullable();
|
||||
$table->boolean('agree_tos')->default(false);
|
||||
});
|
||||
|
||||
DB::table('users')->update(['agree_tos' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('agree_tos');
|
||||
$table->dropColumn('tfa_secret');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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('user_backup_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignUuid('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('code', 256);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_backup_codes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->renameColumn('home_address', 'shipping_address');
|
||||
$table->renameColumn('home_address2', 'shipping_address2');
|
||||
$table->renameColumn('home_city', 'shipping_city');
|
||||
$table->renameColumn('home_state', 'shipping_state');
|
||||
$table->renameColumn('home_postcode', 'shipping_postcode');
|
||||
$table->renameColumn('home_country', 'shipping_country');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->renameColumn('shipping_address', 'home_address');
|
||||
$table->renameColumn('shipping_address2', 'home_address2');
|
||||
$table->renameColumn('shipping_city', 'home_city');
|
||||
$table->renameColumn('shipping_state', 'home_state');
|
||||
$table->renameColumn('shipping_postcode', 'home_postcode');
|
||||
$table->renameColumn('shipping_country', 'home_country');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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('sent_emails', function (Blueprint $table) {
|
||||
$table->string('id', 15)->primary();
|
||||
$table->string('recipient');
|
||||
$table->string('mailable_class');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sent_emails');
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,7 @@ use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use App\Models\Event;
|
||||
use App\Models\Workshop;
|
||||
use Database\Factories\LocationFactory;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
5
lang/vendor/clamav-validator/en/validation.php
vendored
Normal file
5
lang/vendor/clamav-validator/en/validation.php
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'clamav' => ':attribute contains virus.',
|
||||
];
|
||||
1910
package-lock.json
generated
1910
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,14 @@
|
||||
<If "%{HTTP_HOST} =~ /(\.local|^localhost(:\d+)?$)/">
|
||||
# Disable caching in dev
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
Header unset ETag
|
||||
</IfModule>
|
||||
FileETag None
|
||||
</If>
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
# Enable on-the-fly compression for various file types.
|
||||
AddOutputFilterByType DEFLATE application/javascript
|
||||
@@ -54,10 +65,12 @@
|
||||
# Block access to .git directory
|
||||
RewriteRule .*\.git/.* - [L,R=404]
|
||||
|
||||
# Force HTTPS and www subdomain
|
||||
RewriteCond %{HTTPS} off [OR]
|
||||
RewriteCond %{HTTP_HOST} !^www\. [NC]
|
||||
RewriteRule ^ https://www.stemmechanics.com.au%{REQUEST_URI} [L,R=301]
|
||||
# <If "%{HTTP_HOST} !~ /(\.local|^localhost(:\d+)?$)/">
|
||||
# # Force HTTPS and www in prod
|
||||
# RewriteCond %{HTTPS} off [OR]
|
||||
# RewriteCond %{HTTP_HOST} !^www\. [NC]
|
||||
# RewriteRule ^ https://www.stemmechanics.com.au%{REQUEST_URI} [L,R=301]
|
||||
# </If>
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
|
||||
BIN
public/about.webp
Normal file
BIN
public/about.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -16,6 +16,23 @@ let SM = {
|
||||
Swal.fire(data);
|
||||
},
|
||||
|
||||
confirm: (title, content, button, callback) => {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'warning',
|
||||
iconColor: '#b91c1c',
|
||||
title: title,
|
||||
html: content,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: button,
|
||||
confirmButtonColor: '#b91c1c',
|
||||
cancelButtonText: 'Cancel',
|
||||
reverseButtons: true
|
||||
}).then((result) => {
|
||||
callback(result.isConfirmed);
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: (text) => {
|
||||
const copyContent = async () => {
|
||||
try {
|
||||
@@ -29,21 +46,21 @@ let SM = {
|
||||
copyContent().then(() => { /* empty */});
|
||||
},
|
||||
|
||||
updateBillingAddress: () => {
|
||||
const checkboxElement = document.querySelector('input[name="billing_same_home"]');
|
||||
updateShippingAddress: () => {
|
||||
const checkboxElement = document.querySelector('input[name="shipping_same_billing"]');
|
||||
|
||||
if (checkboxElement) {
|
||||
const itemNames = ['address', 'address2', 'city', 'state', 'postcode', 'country'];
|
||||
|
||||
if (checkboxElement.checked) {
|
||||
itemNames.forEach((itemName) => {
|
||||
const element = document.querySelector(`input[name="billing_${itemName}"]`);
|
||||
element.value = document.querySelector(`input[name="home_${itemName}"]`).value;
|
||||
const element = document.querySelector(`input[name="shipping_${itemName}"]`);
|
||||
element.value = document.querySelector(`input[name="billing_${itemName}"]`).value;
|
||||
element.setAttribute('readonly', 'true');
|
||||
});
|
||||
} else {
|
||||
itemNames.forEach((itemName) => {
|
||||
const element = document.querySelector(`input[name="billing_${itemName}"]`);
|
||||
const element = document.querySelector(`input[name="shipping_${itemName}"]`);
|
||||
element.removeAttribute('readonly');
|
||||
});
|
||||
}
|
||||
@@ -347,6 +364,6 @@ let SM = {
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
SM.updateBillingAddress();
|
||||
SM.updateShippingAddress();
|
||||
SM.updateAllThumbnails();
|
||||
});
|
||||
|
||||
@@ -13,11 +13,11 @@ return [
|
||||
'surname_required' => 'A surname is required',
|
||||
'phone_required' => 'A phone number is required',
|
||||
|
||||
'home_address_required' => 'A home address is required',
|
||||
'home_city_required' => 'A home city is required',
|
||||
'home_postcode_required' => 'A home postcode is required',
|
||||
'home_country_required' => 'A home country is required',
|
||||
'home_state_required' => 'A home state is required',
|
||||
'shipping_address_required' => 'A shipping address is required',
|
||||
'shipping_city_required' => 'A shipping city is required',
|
||||
'shipping_postcode_required' => 'A shipping postcode is required',
|
||||
'shipping_country_required' => 'A shipping country is required',
|
||||
'shipping_state_required' => 'A shipping state is required',
|
||||
|
||||
'billing_address_required' => 'A billing address is required',
|
||||
'billing_city_required' => 'A billing city is required',
|
||||
|
||||
19
resources/views/about.blade.php
Normal file
19
resources/views/about.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<x-layout>
|
||||
<x-mast>About STEMMechanics</x-mast>
|
||||
<div class="bg-no-repeat bg-cover h-96" style="background-image:url({{asset('about.webp')}})"></div>
|
||||
<x-container class="pt-8">
|
||||
<p class="mb-4">STEMMechanics is a hands on education studio based in Cairns, created and operated by James Collins. Drawing on years of experience delivering digital learning, STEM programs and creative technology workshops across Queensland, James built STEMMechanics to give communities practical, engaging ways to understand technology.</p>
|
||||
<p class="mb-4">James' background includes work with State Library of Queensland, Education Queensland and a wide range of community organisations, where he delivered digital literacy programs, ICT support, eSports events, STEM initiatives and media workshops in both metropolitan and remote regions. This mix of technical, educational and community based experience shaped the approach that defines STEMMechanics today.</p>
|
||||
<p class="mb-4">At its core, STEMMechanics exists because learners understand technology best when they can build, test, break, remake and explore it themselves. James believes in hands on, curiosity driven learning that develops confidence, problem solving skills and genuine interest in STEM. Every program is designed to be practical, creative and accessible.</p>
|
||||
<p class="mb-4">STEMMechanics operates from a dedicated, private workshop in Cairns where James prototypes new ideas, experiments with electronics and mechanical builds, develops software tools and designs the kits used in his workshops. This workshop is the testing ground for everything delivered to schools and communities across the region.</p>
|
||||
|
||||
<x-heading>What STEMMechanics Does</x-heading>
|
||||
<h3 class="ml-4 font-bold">STEM Workshops</h3>
|
||||
<p class="ml-4 mb-4">STEMMechanics delivers a wide range of workshop programs including coding, robotics, Micro:bit activities, cardboard engineering, mechanical motion, paper circuits and hands on build projects. All sessions are tailored to the age group, learning outcomes and context of each community.</p>
|
||||
<h3 class="ml-4 font-bold">Digital Media & Creative Tech</h3>
|
||||
<p class="ml-4 mb-4">Workshops include stop motion animation, filmmaking, digital storytelling and introductory media production. These programs support students in developing both creative and technical skills.</p>
|
||||
<h3 class="ml-4 font-bold">Community & School Programs</h3>
|
||||
<p class="ml-4 mb-4">STEMMechanics partners with schools, regional councils, libraries and community groups to deliver project based STEM initiatives, themed workshop blocks and multi day programs.</p>
|
||||
|
||||
</x-container>
|
||||
</x-layout>
|
||||
@@ -1,21 +1,21 @@
|
||||
@php
|
||||
$user = auth()->user();
|
||||
|
||||
$billing_same_home = $user->home_address === $user->billing_address
|
||||
&& $user->home_address2 === $user->billing_address2
|
||||
&& $user->home_city === $user->billing_city
|
||||
&& $user->home_state === $user->billing_state
|
||||
&& $user->home_postcode === $user->billing_postcode
|
||||
&& $user->home_country === $user->billing_country;
|
||||
$shipping_same_billing = $user->shipping_address === $user->billing_address
|
||||
&& $user->shipping_address2 === $user->billing_address2
|
||||
&& $user->shipping_city === $user->billing_city
|
||||
&& $user->shipping_state === $user->billing_state
|
||||
&& $user->shipping_postcode === $user->billing_postcode
|
||||
&& $user->shipping_country === $user->billing_country;
|
||||
@endphp
|
||||
|
||||
<x-layout>
|
||||
<x-mast>Account Settings</x-mast>
|
||||
<x-container>
|
||||
<form method="POST" action="{{ route('account.update') }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
|
||||
<form method="POST" action="{{ route('account.update') }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
|
||||
@csrf
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
|
||||
<div class="flex gap-8">
|
||||
<div class="flex flex-col sm:gap-8 sm:flex-row">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="First name" name="firstname" value="{{ $user->firstname }}" />
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@ $billing_same_home = $user->home_address === $user->billing_address
|
||||
<x-ui.input label="Surname" name="surname" value="{{ $user->surname }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-8">
|
||||
<div class="flex flex-col sm:gap-8 sm:flex-row">
|
||||
<div class="flex-1">
|
||||
<x-ui.input type="email" label="Email" name="email" value="{{ $user->email }}" info="{{ $user->email_update_pending ? 'Pending request to change to ' . $user->email_update_pending : '' }}"/>
|
||||
</div>
|
||||
@@ -42,25 +42,77 @@ $billing_same_home = $user->home_address === $user->billing_address
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<section x-data="{ open: false }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}"
|
||||
class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Two Factor Authentication</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.input label="Address" name="home_address" value="{{ $user->home_address }}" />
|
||||
<x-ui.input label="Address 2" name="home_address2" value="{{ $user->home_address2 }}" />
|
||||
<x-ui.input label="City" name="home_city" value="{{ $user->home_city }}" />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="home_state" value="{{ $user->home_state }}" />
|
||||
<div class="px-4 mb-4" x-show="open">
|
||||
<div class="flex items-center border border-gray-300 rounded bg-white pl-2 pr-4 py-3 mb-4">
|
||||
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-envelope text-2xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="home_postcode" value="{{ $user->home_postcode }}" />
|
||||
<div class="mx-4 flex-grow">
|
||||
<p class="flex mb-2">
|
||||
<span class="text-sm font-bold mr-2">Use Email</span>
|
||||
<span class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
|
||||
</p>
|
||||
<p class="text-xs">Use the security link sent to your email address as your two-factor authentication (2FA). The security link will be sent to the address associated with your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded bg-white pl-2 pr-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-mobile-screen-button text-2xl"></i>
|
||||
</div>
|
||||
<div class="mx-4 flex-grow">
|
||||
<p class="flex mb-2">
|
||||
<span class="text-sm font-bold mr-2">Use Authenticator App</span>
|
||||
<span x-cloak x-show="!$store.tfa.enabled" class="text-xs bg-red-500 text-white rounded px-2 py-0.5">Disabled</span>
|
||||
<span x-cloak x-show="$store.tfa.enabled" class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
|
||||
</p>
|
||||
<p class="text-xs">Use an Authenticator App as your two-factor authenticator. When you sign in you'll be asked to use the security code provided by your Authenticator App.</p>
|
||||
</div>
|
||||
<div class="flex flex-col text-nowrap gap-2">
|
||||
<x-ui.button x-show="!$store.tfa.enabled" id="tfa_button" type="button" color="primary-outline" x-data x-on:click.prevent="setupTFA()">Setup</x-ui.button>
|
||||
<x-ui.button x-show="$store.tfa.enabled" type="button" color="danger-outline" x-data x-on:click.prevent="destroyTFA()">Disable</x-ui.button>
|
||||
<a href="#" x-show="$store.tfa.enabled" x-on:click.prevent="resetBackupCodes($event)" class="text-xs link">Reset Backup Codes</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex items-center justify-center gap-4" x-cloak x-show="$store.tfa.show && !$store.tfa.loading">
|
||||
<img src="/loading.gif" id="tfa_image_loader" alt="loading" width="100" height="100"/>
|
||||
<img src="" id="tfa_image" alt="QR Code" width="150" height="150" style="display:none" onload="this.style.display='block';document.getElementById('tfa_image_loader').style.display='none';"/>
|
||||
<div>
|
||||
<p class="text-xs mb-2">Scan the QR Code into your Authenticator App and enter the code provided below</p>
|
||||
<div class="flex items-center gap-4 justify-center">
|
||||
<x-ui.input name="code" id="code" class="mb-0" />
|
||||
<x-ui.button class="mt-1" type="button" color="primary-outline" x-on:click.prevent="linkTFA()">Link</x-ui.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.loading">
|
||||
<img src="/loading.gif" alt="loading" width="100" height="100"/>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.codes && !$store.tfa.loading">
|
||||
<div class="w-[34rem] flex items-center gap-4">
|
||||
<div class="w-[18rem] mx-auto">
|
||||
<p class="text-sm font-bold mb-1">Save your Backup Codes</p>
|
||||
<ul class="ml-6 mb-4 text-xs list-disc">
|
||||
<li>Keep these backup codes safe</li>
|
||||
<li>You can only use each one once</li>
|
||||
<li>They will not be shown again</li>
|
||||
<li>Any existing codes can no longer be used</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="w-[16rem] bg-gray-200 p-4 text-sm font-mono flex flex-wrap justify-center">
|
||||
<template x-for="(code, idx) in $store.tfa.codes" :key="idx">
|
||||
<p class="mx-4" x-text="code"></p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="home_country" value="{{ $user->home_country }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -70,19 +122,40 @@ $billing_same_home = $user->home_address === $user->billing_address
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="{{ $billing_same_home }}" x-data x-on:click="SM.updateBillingAddress" />
|
||||
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" readonly="{{ $billing_same_home }}" />
|
||||
<div class="flex gap-8">
|
||||
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" />
|
||||
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" />
|
||||
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" />
|
||||
<div class="flex flex-col sm:gap-8 sm:flex-row">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Shipping Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="{{ $shipping_same_billing }}" x-data x-on:click="SM.updateShippingAddress" />
|
||||
<x-ui.input label="Address" name="shipping_address" value="{{ $user->shipping_address }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<x-ui.input label="Address 2" name="shipping_address2" value="{{ $user->shipping_address2 }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<x-ui.input label="City" name="shipping_city" value="{{ $user->shipping_city }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<div class="flex flex-col sm:gap-8 sm:flex-row">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="shipping_state" value="{{ $user->shipping_state }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="shipping_postcode" value="{{ $user->shipping_postcode }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="shipping_country" value="{{ $user->shipping_country }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -97,3 +170,106 @@ $billing_same_home = $user->home_address === $user->billing_address
|
||||
</form>
|
||||
</x-container>
|
||||
</x-layout>
|
||||
|
||||
{{ $codes ?? '' }}
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('tfa', {
|
||||
show: false,
|
||||
secret: null,
|
||||
enabled: {{ $user->tfa_secret !== null ? 'true' : 'false'}},
|
||||
codes: null,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
|
||||
function setupTFA() {
|
||||
document.getElementById('tfa_button').disabled = true;
|
||||
axios.get('/account/2fa')
|
||||
.then(response => {
|
||||
if(response.data.secret) {
|
||||
Alpine.store('tfa').show = true;
|
||||
Alpine.store('tfa').secret = response.data.secret;
|
||||
document.getElementById('tfa_image').src = '/account/2fa/image?secret=' + response.data.secret;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function linkTFA() {
|
||||
Alpine.store('tfa').loading = true;
|
||||
axios.post('/account/2fa', {
|
||||
code: document.getElementById('code').value,
|
||||
secret: Alpine.store('tfa').secret,
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
if(response.data.success) {
|
||||
SM.alert('2FA Linked', 'Two-factor authentication has been successfully linked to your account', 'success');
|
||||
document.getElementById('tfa_button').disabled = false;
|
||||
document.getElementById('code').value = '';
|
||||
document.getElementById('tfa_image').src = '';
|
||||
Alpine.store('tfa').show = false;
|
||||
Alpine.store('tfa').enabled = true;
|
||||
Alpine.store('tfa').codes = response.data.codes;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
Alpine.store('tfa').loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function resetBackupCodes(event) {
|
||||
event.target.classList.add('disabled');
|
||||
Alpine.store('tfa').codes = null;
|
||||
Alpine.store('tfa').loading = true;
|
||||
|
||||
axios.post('/account/2fa/reset-backup-codes')
|
||||
.then(response => {
|
||||
if(response.data.success) {
|
||||
Alpine.store('tfa').codes = response.data.codes;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
event.target.classList.remove('disabled');
|
||||
Alpine.store('tfa').loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function destroyTFA() {
|
||||
SM.confirm('Disable 2FA', 'Are you sure you want to remove two-factor authentication from your account?', 'Disable', (confirm) => {
|
||||
if(confirm) {
|
||||
axios.delete('/account/2fa')
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
SM.alert('2FA Disabled', 'Two-factor authentication has been successfully disabled on your account', 'success');
|
||||
Alpine.store('tfa').enabled = false;
|
||||
Alpine.store('tfa').codes = null;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
confirmButtonText: 'Disable'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<x-mast backRoute="admin.user.index" backTitle="Users">Create User</x-mast>
|
||||
|
||||
<x-container>
|
||||
<form method="POST" action="{{ route('admin.user.store') }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
|
||||
<form method="POST" action="{{ route('admin.user.store') }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
|
||||
@csrf
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
|
||||
<div class="flex gap-8">
|
||||
@@ -22,34 +22,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.input label="Address" name="home_address" />
|
||||
<x-ui.input label="Address 2" name="home_address2" />
|
||||
<x-ui.input label="City" name="home_city" />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="home_state" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="home_postcode" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="home_country" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="true" x-data x-on:click="SM.updateBillingAddress" />
|
||||
<x-ui.input label="Address" name="billing_address" />
|
||||
<x-ui.input label="Address 2" name="billing_address2" />
|
||||
<x-ui.input label="City" name="billing_city" />
|
||||
@@ -65,6 +43,28 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Shipping Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="true" x-data x-on:click="SM.updateShippingAddress" />
|
||||
<x-ui.input label="Address" name="shipping_address" />
|
||||
<x-ui.input label="Address 2" name="shipping_address2" />
|
||||
<x-ui.input label="City" name="shipping_city" />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="shipping_state" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="shipping_postcode" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="shipping_country" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex justify-end mt-8">
|
||||
<x-ui.button type="submit">Create</x-ui.button>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
@props(['user'])
|
||||
|
||||
@php
|
||||
$billing_same_home = $user->home_address === $user->billing_address
|
||||
&& $user->home_address2 === $user->billing_address2
|
||||
&& $user->home_city === $user->billing_city
|
||||
&& $user->home_state === $user->billing_state
|
||||
&& $user->home_postcode === $user->billing_postcode
|
||||
&& $user->home_country === $user->billing_country;
|
||||
$shipping_same_billing = $user->shipping_address === $user->billing_address
|
||||
&& $user->shipping_address2 === $user->billing_address2
|
||||
&& $user->shipping_city === $user->billing_city
|
||||
&& $user->shipping_state === $user->billing_state
|
||||
&& $user->shipping_postcode === $user->billing_postcode
|
||||
&& $user->shipping_country === $user->billing_country;
|
||||
@endphp
|
||||
|
||||
<x-layout>
|
||||
<x-mast backRoute="admin.user.index" backTitle="Users">Edit User</x-mast>
|
||||
|
||||
<x-container>
|
||||
<form method="POST" action="{{ route('admin.user.update', $user) }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
|
||||
<form method="POST" action="{{ route('admin.user.update', $user) }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
|
||||
@@ -34,14 +34,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ $user }}
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Email Subscriptions</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Upcoming Workshops" name="billing_same_home" checked="{{ $billing_same_home }}" />
|
||||
<x-ui.checkbox label="Upcoming Workshops" name="subscribed" checked="{{ $user->subscribed }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -51,18 +50,18 @@
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.input label="Address" name="home_address" value="{{ $user->home_address }}" />
|
||||
<x-ui.input label="Address 2" name="home_address2" value="{{ $user->home_address2 }}" />
|
||||
<x-ui.input label="City" name="home_city" value="{{ $user->home_city }}" />
|
||||
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" />
|
||||
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" />
|
||||
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="home_state" value="{{ $user->home_state }}" />
|
||||
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="home_postcode" value="{{ $user->home_postcode }}" />
|
||||
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="home_country" value="{{ $user->home_country }}" />
|
||||
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -72,19 +71,19 @@
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
|
||||
</a>
|
||||
<div x-show="open">
|
||||
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="{{ $billing_same_home }}" x-data x-on:click="SM.updateBillingAddress" />
|
||||
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="{{ $shipping_same_billing }}" x-data x-on:click="SM.updateShippingAddress" />
|
||||
<x-ui.input label="Address" name="shipping_address" value="{{ $user->shipping_address }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<x-ui.input label="Address 2" name="shipping_address2" value="{{ $user->shipping_address2 }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<x-ui.input label="City" name="shipping_city" value="{{ $user->shipping_city }}" readonly="{{ $shipping_same_billing }}" />
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="State" name="shipping_state" value="{{ $user->shipping_state }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Postcode" name="shipping_postcode" value="{{ $user->shipping_postcode }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
</div>
|
||||
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" readonly="{{ $billing_same_home }}" />
|
||||
<x-ui.input label="Country" name="shipping_country" value="{{ $user->shipping_country }}" readonly="{{ $shipping_same_billing }}" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
@php
|
||||
$eventContent = isset($event) ? $event->content : '';
|
||||
$workshopContent = isset($workshop) ? $workshop->content : '';
|
||||
@endphp
|
||||
<x-layout>
|
||||
<x-mast backRoute="admin.event.index" backTitle="Workshops">{{ isset($event) ? 'Edit' : 'Create' }} Workshop</x-mast>
|
||||
<x-mast backRoute="admin.workshop.index" backTitle="Workshops">{{ isset($workshop) ? 'Edit' : 'Create' }} Workshop</x-mast>
|
||||
|
||||
<x-container class="mt-4">
|
||||
<form x-data="{type:'physical',registration:'{{old('registration', $event->registration ?? 'none')}}'}" method="POST" action="{{ route('admin.event.' . (isset($event) ? 'update' : 'store'), $event ?? []) }}">
|
||||
@isset($event)
|
||||
<form x-data="{type:'physical',registration:'{{old('registration', $workshop->registration ?? 'none')}}'}" method="POST" action="{{ route('admin.workshop.' . (isset($workshop) ? 'update' : 'store'), $workshop ?? []) }}">
|
||||
@isset($workshop)
|
||||
@method('PUT')
|
||||
@endisset
|
||||
@csrf
|
||||
<div class="mb-4">
|
||||
<x-ui.input label="Title" name="title" value="{!! isset($event) ? $event->title : '' !!}" />
|
||||
<x-ui.input label="Title" name="title" value="{!! isset($workshop) ? $workshop->title : '' !!}" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<x-ui.media label="Image" name="hero_media_name" value="{{ $event->hero_media_name ?? '' }}" allow_uploads="true" />
|
||||
<x-ui.media label="Image" name="hero_media_name" value="{{ $workshop->hero_media_name ?? '' }}" allow_uploads="true" />
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.select label="Type" name="type" x-model="type">
|
||||
<option value="physical" {{ ($event->location_id ?? '') !== '' || !isset($event) ? 'selected' : '' }}>Physical</option>
|
||||
<option value="online" {{ ($event->location_id ?? '') === null ? 'selected' : '' }}>Online</option>
|
||||
<option value="physical" {{ ($workshop->location_id ?? '') !== '' || !isset($workshop) ? 'selected' : '' }}>Physical</option>
|
||||
<option value="online" {{ ($workshop->location_id ?? '') === null ? 'selected' : '' }}>Online</option>
|
||||
</x-ui.select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span x-show="type==='physical'">
|
||||
<x-ui.select label="Location" name="location_id">
|
||||
@foreach(\App\Models\Location::orderByRaw("name = 'Online' DESC, name ASC")->get() as $location)
|
||||
<option value="{{ $location->id }}" {{ ($event->location_id ?? '') === $location->id ? 'selected' : '' }}>{{ $location->name }}</option>
|
||||
<option value="{{ $location->id }}" {{ ($workshop->location_id ?? '') === $location->id ? 'selected' : '' }}>{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</x-ui.select>
|
||||
</span>
|
||||
@@ -35,26 +35,26 @@
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input type="datetime-local" label="Start Date" name="starts_at" value="{{ \App\Helpers::timestampNoSeconds($event->starts_at ?? '') }}" onchange="updatedStartsAt()"/>
|
||||
<x-ui.input type="datetime-local" label="Start Date" name="starts_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->starts_at ?? '') }}" onchange="updatedStartsAt()"/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input type="datetime-local" label="End Date" name="ends_at" value="{{ \App\Helpers::timestampNoSeconds($event->ends_at ?? '') }}" />
|
||||
<x-ui.input type="datetime-local" label="End Date" name="ends_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->ends_at ?? '') }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.select label="Status" name="status">
|
||||
<option value="draft" {{ ($event->status ?? '') === 'draft' ? 'selected' : '' }}>Draft</option>
|
||||
<option value="open" {{ ($event->status ?? '') === 'open' ? 'selected' : '' }}>Open</option>
|
||||
<option value="private" {{ ($event->status ?? '') === 'private' ? 'selected' : '' }}>Private</option>
|
||||
<option value="full" {{ ($event->status ?? '') === 'full' ? 'selected' : '' }}>Full</option>
|
||||
<option value="scheduled" {{ ($event->status ?? '') === 'scheduled' ? 'selected' : '' }}>Scheduled</option>
|
||||
<option value="closed" {{ ($event->status ?? '') === 'closed' ? 'selected' : '' }}>Closed</option>
|
||||
<option value="cancelled" {{ ($event->status ?? '') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
<option value="draft" {{ ($workshop->status ?? '') === 'draft' ? 'selected' : '' }}>Draft</option>
|
||||
<option value="open" {{ ($workshop->status ?? '') === 'open' ? 'selected' : '' }}>Open</option>
|
||||
<option value="private" {{ ($workshop->status ?? '') === 'private' ? 'selected' : '' }}>Private</option>
|
||||
<option value="full" {{ ($workshop->status ?? '') === 'full' ? 'selected' : '' }}>Full</option>
|
||||
<option value="scheduled" {{ ($workshop->status ?? '') === 'scheduled' ? 'selected' : '' }}>Scheduled</option>
|
||||
<option value="closed" {{ ($workshop->status ?? '') === 'closed' ? 'selected' : '' }}>Closed</option>
|
||||
<option value="cancelled" {{ ($workshop->status ?? '') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
|
||||
</x-ui.select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input type="datetime-local" label="Publish Date" name="publish_at" value="{{ \App\Helpers::timestampNoSeconds($event->publish_at ?? '') }}" onchange="updatedPublishAt()" />
|
||||
<x-ui.input type="datetime-local" label="Publish Date" name="publish_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->publish_at ?? '') }}" onchange="updatedPublishAt()" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
@@ -62,44 +62,44 @@
|
||||
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input type="datetime-local" label="Closes Date" name="closes_at" value="{{ \App\Helpers::timestampNoSeconds($event->closes_at ?? '') }}" />
|
||||
<x-ui.input type="datetime-local" label="Closes Date" name="closes_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->closes_at ?? '') }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Price" name="price" info="Leave blank to hide from public. Also supports Free, TBD or TBC" value="{{ $event->price ?? '' }}" />
|
||||
<x-ui.input label="Price" name="price" info="Leave blank to hide from public. Also supports Free, TBD or TBC" value="{{ $workshop->price ?? '' }}" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.input label="Ages" name="ages" info="Leave blank to hide from public" value="{{ $event->ages ?? '8+' }}" />
|
||||
<x-ui.input label="Ages" name="ages" info="Leave blank to hide from public" value="{{ $workshop->ages ?? '8+' }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:gap-8">
|
||||
<div class="flex-1">
|
||||
<x-ui.select label="Registration" name="registration" x-model="registration" onchange="document.getElementsByName('registration_data').forEach((e)=>e.value='')">
|
||||
<option value="none" {{ (old('registration', $event->registration ?? '')) === 'none' ? 'selected' : '' }}>None</option>
|
||||
<option value="link" {{ (old('registration', $event->registration ?? '')) === 'link' ? 'selected' : '' }}>External Link</option>
|
||||
<option value="email" {{ (old('registration', $event->registration ?? '')) === 'email' ? 'selected' : '' }}>External Email</option>
|
||||
<option value="message" {{ (old('registration', $event->registration ?? '')) === 'message' ? 'selected' : '' }}>Custom Message</option>
|
||||
<option value="none" {{ (old('registration', $workshop->registration ?? '')) === 'none' ? 'selected' : '' }}>None</option>
|
||||
<option value="link" {{ (old('registration', $workshop->registration ?? '')) === 'link' ? 'selected' : '' }}>External Link</option>
|
||||
<option value="email" {{ (old('registration', $workshop->registration ?? '')) === 'email' ? 'selected' : '' }}>External Email</option>
|
||||
<option value="message" {{ (old('registration', $workshop->registration ?? '')) === 'message' ? 'selected' : '' }}>Custom Message</option>
|
||||
</x-ui.select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span x-show="registration==='link'">
|
||||
<x-ui.input label="Registration URL" name="registration_url" id="registration_url" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
|
||||
<x-ui.input label="Registration URL" name="registration_url" id="registration_url" value="{!! isset($workshop) ? $workshop->registration_data : '' !!}" error="{{ $errors->first('registration_data') }}" />
|
||||
</span>
|
||||
<span x-show="registration==='email'">
|
||||
<x-ui.input label="Registration Email" name="registration_email" id="registration_email" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
|
||||
<x-ui.input label="Registration Email" name="registration_email" id="registration_email" value="{{ $workshop->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
|
||||
</span>
|
||||
<span x-show="registration==='message'">
|
||||
<x-ui.input label="Registration Message" name="registration_message" id="registration_message" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
|
||||
<x-ui.input label="Registration Message" name="registration_message" id="registration_message" value="{{ $workshop->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
|
||||
</span>
|
||||
<input type="hidden" name="registration_data" id="registration_data" value="{{ $event->registration_data ?? '' }}">
|
||||
<input type="hidden" name="registration_data" id="registration_data" value="{{ $workshop->registration_data ?? '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<x-ui.editor
|
||||
label="Content"
|
||||
name="content"
|
||||
value="{!! $eventContent !!}"
|
||||
value="{!! $workshopContent !!}"
|
||||
></x-ui.editor>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
@@ -107,14 +107,14 @@
|
||||
label="Files"
|
||||
name="files"
|
||||
editor="true"
|
||||
value="{!! isset($event) ? $event->files()->orderBy('name')->get() : '' !!}"
|
||||
value="{!! isset($workshop) ? $workshop->files()->orderBy('name')->get() : '' !!}"
|
||||
></x-ui.filelist>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4 mt-8">
|
||||
@isset($event)
|
||||
<x-ui.button type="button" color="danger" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.event.destroy', $event) }}')">Delete</x-ui.button>
|
||||
@isset($workshop)
|
||||
<x-ui.button type="button" color="danger" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.workshop.destroy', $workshop) }}')">Delete</x-ui.button>
|
||||
@endisset
|
||||
<x-ui.button type="submit">{{ isset($event) ? 'Save' : 'Create' }}</x-ui.button>
|
||||
<x-ui.button type="submit">{{ isset($workshop) ? 'Save' : 'Create' }}</x-ui.button>
|
||||
</div>
|
||||
</form>
|
||||
</x-container>
|
||||
@@ -4,14 +4,14 @@
|
||||
<x-container>
|
||||
<div class="flex my-4 items-center">
|
||||
<div class="flex-1">
|
||||
<x-ui.button type="link" href="{{ route('admin.event.create') }}">Create</x-ui.button>
|
||||
<x-ui.button type="link" href="{{ route('admin.workshop.create') }}">Create</x-ui.button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<x-ui.search name="search" label="Search" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($events->isEmpty())
|
||||
@if($workshops->isEmpty())
|
||||
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
|
||||
@else
|
||||
<x-ui.table>
|
||||
@@ -23,24 +23,24 @@
|
||||
<th>Action</th>
|
||||
</x-slot:header>
|
||||
<x-slot:body>
|
||||
@foreach ($events as $event)
|
||||
@foreach ($workshops as $workshop)
|
||||
<tr>
|
||||
<td class="flex items-center">
|
||||
<img src="{{ $event->hero->thumbnail }}" class="max-h-12 max-w-12 -ml-2 -my-3 mr-3 inline rounded" alt="{{ $event->hero->title }}" />
|
||||
<img src="{{ $workshop->hero->thumbnail }}" class="max-h-12 max-w-12 -ml-2 -my-3 mr-3 inline rounded" alt="{{ $workshop->hero->title }}" />
|
||||
<div>
|
||||
<div class="whitespace-normal">{{ $event->title }}</div>
|
||||
<div class="lg:hidden text-xs text-gray-500">{{ $event->location->name }} ({{ ucwords($event->status) }})</div>
|
||||
<div class="md:hidden text-xs text-gray-500">{{ \Carbon\Carbon::parse($event->starts_at)->format('j/m/Y g:i a') }}</div>
|
||||
<div class="whitespace-normal">{{ $workshop->title }}</div>
|
||||
<div class="lg:hidden text-xs text-gray-500">{{ $workshop->location->name }} ({{ ucwords($workshop->status) }})</div>
|
||||
<div class="md:hidden text-xs text-gray-500">{{ \Carbon\Carbon::parse($workshop->starts_at)->format('j/m/Y g:i a') }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden lg:table-cell">{{ ucwords($event->status) }}</td>
|
||||
<td class="hidden lg:table-cell">{{ $event->location->name }}</td>
|
||||
<td class="hidden md:table-cell">{{ \Carbon\Carbon::parse($event->starts_at)->format('M j Y, g:i a') }}</td>
|
||||
<td class="hidden lg:table-cell">{{ ucwords($workshop->status) }}</td>
|
||||
<td class="hidden lg:table-cell">{{ $workshop->location->name }}</td>
|
||||
<td class="hidden md:table-cell">{{ \Carbon\Carbon::parse($workshop->starts_at)->format('M j Y, g:i a') }}</td>
|
||||
<td>
|
||||
<div class="flex justify-center gap-3">
|
||||
<a href="{{ route('admin.event.edit', $event) }}" class="hover:text-primary-color" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||
<a href="{{ route('admin.event.duplicate', $event) }}" class="hover:text-primary-color" title="Duplicate"><i class="fa-regular fa-copy"></i></a>
|
||||
<a href="#" class="hover:text-red-600" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.event.destroy', $event) }}')" title="Delete"><i class="fa-solid fa-trash"></i></a>
|
||||
<a href="{{ route('admin.workshop.edit', $workshop) }}" class="hover:text-primary-color" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||
<a href="{{ route('admin.workshop.duplicate', $workshop) }}" class="hover:text-primary-color" title="Duplicate"><i class="fa-regular fa-copy"></i></a>
|
||||
<a href="#" class="hover:text-red-600" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.workshop.destroy', $workshop) }}')" title="Delete"><i class="fa-solid fa-trash"></i></a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -48,7 +48,7 @@
|
||||
</x-slot:body>
|
||||
</x-ui.table>
|
||||
|
||||
{{ $events->appends(request()->query())->links() }}
|
||||
{{ $workshops->appends(request()->query())->links() }}
|
||||
@endif
|
||||
|
||||
</x-container>
|
||||
68
resources/views/auth/login-2fa.blade.php
Normal file
68
resources/views/auth/login-2fa.blade.php
Normal file
@@ -0,0 +1,68 @@
|
||||
@php
|
||||
if(!isset($email)) {
|
||||
$email = '';
|
||||
if(isset($user)) {
|
||||
$email = $user->email;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<div x-data="{show:'{{ $method ?? 'tfa' }}'}">
|
||||
<x-dialog x-cloak x-show="show==='tfa'" formaction="{{ route('login.store') }}">
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="{{ route('login') }}"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Please enter 2FA code
|
||||
</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p class="text-sm">Two-factor authentication (2FA) is enabled for your account. Please enter a code to log in.</p>
|
||||
</x-slot:header>
|
||||
<input type="hidden" name="email" value="{{ $email }}"/>
|
||||
<x-ui.input type="text" name="code" label="Code" floating autofocus error="{{ $errors->first('code') }}"/>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">
|
||||
Having trouble? <a class="link" href="#" x-on:click.prevent="show='other'">Sign in another way</a>
|
||||
</div>
|
||||
<x-ui.button type="submit">Verify</x-ui.button>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
<x-dialog x-cloak x-show="show==='other'">
|
||||
@captcha
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="#" x-on:click.prevent="show='tfa'"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Sign in another way
|
||||
</x-slot:title>
|
||||
<x-slot:header>Select the method to sign in to your account</x-slot:header>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<form method="post" action="{{ route('login.store') }}">
|
||||
@csrf
|
||||
@captcha
|
||||
<input type="hidden" name="email" value="{{ $email }}" />
|
||||
<input type="hidden" name="method" value="email" />
|
||||
<x-ui.button type="submit" class="w-full">Email Link</x-ui.button>
|
||||
</form>
|
||||
<x-ui.button type="button" x-on:click.prevent="show='backup'">Enter Backup Code</x-ui.button>
|
||||
</div>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
<x-dialog x-cloak x-show="show==='backup'" formaction="{{ route('login.store') }}">
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="#" x-on:click.prevent="show='other'"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Please enter a backup code
|
||||
</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p class="text-sm">Enter one of your backup codes below to log in. Once a backup codes are a 1 time use only.</p>
|
||||
</x-slot:header>
|
||||
@captcha
|
||||
<input type="hidden" name="email" value="{{ $email }}"/>
|
||||
<x-ui.input type="text" name="backup_code" label="Backup Code" floating autofocus error="{{ $errors->first('backup_code') }}" />
|
||||
<x-slot:footer center>
|
||||
<x-ui.button class="self-end" type="submit">Verify</x-ui.button>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
</div>
|
||||
</x-layout>
|
||||
14
resources/views/auth/login-alt.blade.php
Normal file
14
resources/views/auth/login-alt.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<x-dialog formaction="{{ route('login.store') }}">
|
||||
@captcha
|
||||
<x-slot:title>Sign in another way</x-slot:title>
|
||||
<x-slot:header>Select the method to sign in to your account</x-slot:header>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<x-ui.button type="button" onclick="loginUsingEmail()">Email Link</x-ui.button>
|
||||
<x-ui.button type="button" onclick="loginUsingEmail()">Enter Backup Code</x-ui.button>
|
||||
</div>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
</x-layout>
|
||||
@@ -1,5 +1,6 @@
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<x-dialog formaction="{{ route('login.store') }}">
|
||||
@captcha
|
||||
@if(session('status') == 'not-found')
|
||||
<x-slot:title>Sorry, we didn't recognize that email</x-slot:title>
|
||||
<x-slot:header>
|
||||
@@ -8,7 +9,7 @@
|
||||
@else
|
||||
<x-slot:title>Sign in with email</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p>Enter the email address associated with your account, and we'll send a magic link to your inbox.</p>
|
||||
<p>Enter the email address associated with your account</p>
|
||||
</x-slot:header>
|
||||
@endif
|
||||
<x-ui.input type="email" name="email" label="Email" floating autofocus />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex items-center justify-center flex-grow py-24">
|
||||
<div class="flex items-center justify-center flex-grow py-24" {{ $attributes }}>
|
||||
<div class="w-full mx-2 max-w-lg p-8 pb-6 bg-white rounded-md shadow-deep">
|
||||
@isset($title)
|
||||
<h2 class="text-2xl font-bold mb-4 text-center">{{ $title }}</h2>
|
||||
<h2 class="text-2xl font-bold mb-4 text-center relative">{{ $title }}</h2>
|
||||
@endisset
|
||||
@isset($header)
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</ul>
|
||||
<ul class="sm:w-1/3 flex flex-col gap-0.5 text-center sm:text-left">
|
||||
<li><h3 class="font-bold mb-2">STEMMechanics</h3></li>
|
||||
<li><a href="{{ route('about') }}" class="text-sm hover:text-primary-color">About</a></li>
|
||||
<li><a href="{{ route('contact') }}" class="text-sm hover:text-primary-color">Contact Us</a></li>
|
||||
<li><a href="{{ route('code-of-conduct') }}" class="text-sm hover:text-primary-color">Code of Conduct</a></li>
|
||||
<li><a href="{{ route('terms-conditions') }}" class="text-sm hover:text-primary-color">Terms & Conditions</a></li>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
</script>
|
||||
@endif
|
||||
@stack('scripts')
|
||||
@captchaScripts
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
<x-container class="bg-primary-color-light text-white py-10">
|
||||
<h1 class="font-bold text-4xl">{{ $slot }}</h1>
|
||||
<h1 class="font-bold text-4xl">{{ $title ?? $slot }}</h1>
|
||||
@if(isset($description))
|
||||
<div class="text-lg">{{ $description }}</div>
|
||||
@endif
|
||||
@if(isset($backRoute) && isset($backTitle))
|
||||
<a href="{{ route($backRoute) }}" class="text-lg hover:text-gray-300"><i class="fa-solid fa-angle-left mr-3"></i>{{ $backTitle }}</a>
|
||||
@endif
|
||||
@isset($tabs)
|
||||
<div class="mt-4 -mb-10 flex justify-end">
|
||||
@foreach($tabs as $tab)
|
||||
<a href="{{ $tab['route'] }}" class="rounded-t-md px-4 py-2 {{ ('/' . request()->path() === parse_url($tab['route'], PHP_URL_PATH) ? 'bg-gray-100 text-primary-color-dark' : 'text-white hover:bg-primary-color-dark') }} transition-colors">{{ $tab['title'] }}</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endisset
|
||||
</x-container>
|
||||
|
||||
@@ -1,60 +1,137 @@
|
||||
<nav class="shadow bg-white">
|
||||
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8 relative" x-data="{open:false}">
|
||||
<nav class="shadow bg-white" x-data="{showSearch:false}" x-init="
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'f') {
|
||||
event.preventDefault();
|
||||
$data.showSearch = true;
|
||||
}
|
||||
})
|
||||
">
|
||||
<div class="mx-auto max-w-7xl px-2 relative" x-data="{pageMenuOpen:false,userMenuOpen:false}">
|
||||
<div class="relative flex h-16 items-center justify-between">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
<!-- Mobile menu button-->
|
||||
<button type="button" class="relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="absolute -inset-0.5"></span>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
{{-- <img src="/assets/logo.svg" alt="STEMMechanics" onload="SVGInject(this)" style="color:purple"/>--}}
|
||||
<div class="ml-4 mr-2 {{ !auth()->user()?->admin ? 'sm:hidden' : '' }}">
|
||||
<button type="button" @click="pageMenuOpen=!pageMenuOpen" @keydown.escape="pageMenuOpen=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">Open page menu</span>
|
||||
<i class="fa fa-bars text-gray-800 hover:text-sky-500 transition"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div class="flex flex-1 items-center justify-center sm:justify-start ml-2">
|
||||
<div class="flex flex-shrink-0 items-center">
|
||||
<a href="{{ route('index') }}">
|
||||
@includeSVG('logo.svg', 'width:14rem;margin-top:-0.2rem;color:black')
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
<div class="flex items-center">
|
||||
<div class="hidden sm:ml-6 sm:block mr-4">
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ route('post.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out transform hover:-translate-y-0.5" aria-current="page">Blog</a>
|
||||
<a href="{{ route('event.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out transform hover:-translate-y-0.5">Workshops</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div>
|
||||
<button type="button" @click="open=!open" @keydown.escape="open=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<svg class="w-6 h-6 text-gray-800 hover:text-sky-500 transition" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M5 7h14M5 12h14M5 17h14"/>
|
||||
</svg>
|
||||
{{-- <a href="{{ route('post.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out" aria-current="page">Blog</a>--}}
|
||||
<a href="{{ route('about') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">About</a>
|
||||
<a href="{{ route('workshop.index') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">Workshops</a>
|
||||
<a href="{{ route('contact') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">Contact</a>
|
||||
<button type="button" class="text-gray-900 hover:text-sky-500 text-sm md:pl-1 font-medium transition duration-300 ease-in-out" @click.prevent="showSearch=true">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-3 md:mx-3">
|
||||
<button type="button" @click="userMenuOpen=!userMenuOpen" @keydown.escape="userMenuOpen=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<i class="fa-regular fa-user-circle text-gray-800 hover:text-sky-500 transition"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="open" @click.away="open=false" x-cloak class="absolute w-full right-0 sm:right-5 sm:top-9 z-50 sm:mt-2 sm:w-48 origin-top-right sm:rounded-md bg-white py-3 px-2 shadow-lg border-t sm:ring-1 ring-black ring-opacity-25 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
|
||||
<a href="{{ route('post.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Blog</a>
|
||||
<a href="{{ route('event.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
|
||||
<div class="sm:hidden border-t border-gray-200 my-2"></div>
|
||||
@if(auth()->guest())
|
||||
<a href="{{ route('register') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-pen-to-square w-4 mr-2"></i>Register</a>
|
||||
<a href="{{ route('login') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-to-bracket w-4 mr-2"></i>Log in</a>
|
||||
@else
|
||||
|
||||
<div x-show="pageMenuOpen" @click.away="pageMenuOpen=false" x-cloak class="fixed left-0 top-0 h-full w-full z-20" role="menu" aria-labelledby="page-menu-button" tabindex="-1">
|
||||
<div x-show="pageMenuOpen" @click="pageMenuOpen=false" class="absolute inset-0 bg-black bg-opacity-40 backdrop-blur-sm"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"></div>
|
||||
<div x-show="pageMenuOpen" class="relative h-full left-0 top-0 w-96 max-w-full bg-white z-50 shadow-lg p-4"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-full"
|
||||
x-transition:enter-end="opacity-100 transform translate-x-0"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 transform translate-x-0"
|
||||
x-transition:leave-end="opacity-0 transform -translate-x-full">
|
||||
<div class="flex justify-between mb-4">
|
||||
<div>
|
||||
@includeSVG('logo.svg', 'width:10em;color:black')
|
||||
</div>
|
||||
<button @click="pageMenuOpen=false" class="hover:text-red-500">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1" @click.prevent="showSearch=true">
|
||||
<i class="fa fa-search w-4 mr-2"></i>Search
|
||||
</div>
|
||||
{{-- <a href="{{ route('post.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Blog</a>--}}
|
||||
<a href="{{ route('about') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-circle-info w-4 mr-2"></i>About</a>
|
||||
<a href="{{ route('workshop.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
|
||||
<a href="{{ route('contact') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-envelope w-4 mr-2"></i>Contact</a>
|
||||
@if(auth()->user()?->admin)
|
||||
<div class="sm:hidden border-t border-gray-200 my-2"></div>
|
||||
<div class="block text-xs font-semibold text-gray-500 px-2 py-1">Admin</div>
|
||||
<a href="{{ route('admin.location.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-location-dot w-4 mr-2"></i>Locations</a>
|
||||
<a href="{{ route('admin.media.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-photo-film w-4 mr-2"></i>Media</a>
|
||||
<a href="{{ route('admin.post.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Posts</a>
|
||||
{{-- <a href="{{ route('admin.post.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Posts</a>--}}
|
||||
<a href="{{ route('admin.user.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-users w-4 mr-2"></i>Users</a>
|
||||
<a href="{{ route('admin.event.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-calendar w-4 mr-2"></i>Events</a>
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<a href="{{ route('admin.workshop.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
|
||||
@endif
|
||||
<a href="{{ route('account.show') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-user-pen w-4 mr-2"></i>Account</a>
|
||||
<a href="{{ route('logout') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-from-bracket w-4 mr-2"></i>Log out</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
x-show="userMenuOpen"
|
||||
@click.away="userMenuOpen=false"
|
||||
x-cloak
|
||||
>
|
||||
<div x-show="userMenuOpen" @click="userMenuOpen=false" class="fixed left-0 w-screen z-20 h-screen bg-black bg-opacity-40 backdrop-blur-sm"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"></div>
|
||||
<div
|
||||
x-show="userMenuOpen"
|
||||
class="absolute w-full right-0 sm:right-5 sm:top-12 z-50 sm:mt-2 sm:w-64 origin-top-right sm:rounded-md bg-white py-3 px-2 shadow-lg border-t sm:ring-1 ring-black ring-opacity-25 focus:outline-none">
|
||||
@if(auth()->guest())
|
||||
<a href="{{ route('register') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-pen-to-square w-4 mr-2"></i>Register</a>
|
||||
<a href="{{ route('login') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-to-bracket w-4 mr-2"></i>Log in</a>
|
||||
@else
|
||||
<div class="text-lg font-semibold px-4 py-1 text-gray-700">Welcome {{ auth()->user()->firstname ?? strstr(auth()->user()->email, '@', true) }}</div>
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<a href="{{ route('account.show') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-user-pen w-4 mr-2"></i>Account</a>
|
||||
<a href="{{ route('logout') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-from-bracket w-4 mr-2"></i>Log out</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center" x-cloak x-show="showSearch" x-on:click="showSearch=false" x-on:keydown.escape.window="showSearch=false" x-init="$watch('showSearch', value => {
|
||||
if(value) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementsByName('q')[0];
|
||||
if (!el) return;
|
||||
el.focus({ preventScroll: true });
|
||||
if (typeof el.select === 'function') el.select();
|
||||
// iOS fallback:
|
||||
if (el.setSelectionRange) el.setSelectionRange(0, el.value.length);
|
||||
})
|
||||
})
|
||||
}
|
||||
})">
|
||||
<div class="absolute inset-0 backdrop-blur-sm bg-opacity-40 bg-black"></div>
|
||||
<div class="relative w-full mx-8 max-w-2xl bg-gray-50 p-2 rounded-lg shadow-lg" x-on:click.stop>
|
||||
<form action="{{ route('search.index') }}" method="GET">
|
||||
<x-ui.search type="text" name="q" label="Search..." />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
@props(['event'])
|
||||
@props(['workshop'])
|
||||
|
||||
@php
|
||||
$statusClass = $event->status;
|
||||
$statusTitle = $event->status;
|
||||
$statusClass = $workshop->status;
|
||||
$statusTitle = $workshop->status;
|
||||
|
||||
if($event->status === 'scheduled') {
|
||||
if($workshop->status === 'scheduled') {
|
||||
$statusClass = 'soon';
|
||||
$statusTitle = 'Open soon';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<a href="{{ route('event.show', $event) }}" class="flex flex-col bg-white border rounded-lg overflow-hidden hover:shadow-lg hover:scale-[101%] transition-all relative {{ $attributes->get('class') }}">
|
||||
<a href="{{ route('workshop.show', $workshop) }}" class="flex flex-col bg-white border rounded-lg overflow-hidden hover:shadow-lg hover:scale-[101%] transition-all relative {{ $attributes->get('class') }}">
|
||||
<div class="shadow border rounded px-3 py-2 absolute top-2 left-2 flex flex-col justify-center items-center bg-white">
|
||||
<div class="text-gray-600 font-bold leading-none">{{ $event->starts_at->format('j') }}</div>
|
||||
<div class="text-gray-600 text-xs uppercase">{{ $event->starts_at->format('M') }}</div>
|
||||
<div class="text-gray-600 font-bold leading-none">{{ $workshop->starts_at->format('j') }}</div>
|
||||
<div class="text-gray-600 text-xs uppercase">{{ $workshop->starts_at->format('M') }}</div>
|
||||
</div>
|
||||
<div class="border border-white border-opacity-50 absolute flex items-center justify-center top-5 -right-9 bg-gray-500 w-36 text-sm text-white font-bold uppercase py-1 rotate-45 h-8 sm-banner-{{ strtolower($statusClass) }}">{{ $statusTitle }}</div>
|
||||
<img src="{{ $event->hero?->url }}?md" alt="{{ $event->title }}" class="w-full h-64 object-cover object-center">
|
||||
<img src="{{ $workshop->hero?->url }}?md" alt="{{ $workshop->title }}" class="w-full h-64 object-cover object-center">
|
||||
<div class="flex-grow p-4 flex flex-col">
|
||||
<h2 class="flex-grow {{ strlen($event->title) > 25 ? 'text-lg' : 'text-xl' }} font-bold mb-2">{{ $event->title }}</h2>
|
||||
<h2 class="flex-grow {{ strlen($workshop->title) > 25 ? 'text-lg' : 'text-xl' }} font-bold mb-2">{{ $workshop->title }}</h2>
|
||||
<div class="text-gray-600 text-sm mb-1 flex gap-2">
|
||||
<div class="w-6 flex items-center justify-center">
|
||||
<i class="fa-regular fa-calendar"></i>
|
||||
</div>{{ $event->starts_at->format('j/m/Y @ g:i a') }}
|
||||
</div>{{ $workshop->starts_at->format('j/m/Y @ g:i a') }}
|
||||
</div>
|
||||
<div class="text-gray-600 text-sm mb-1 flex gap-2">
|
||||
<div class="w-6 flex items-center justify-center">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
</div>{{ $event->location->name }}
|
||||
</div>{{ $workshop->location->name }}
|
||||
</div>
|
||||
@if($event->ages)
|
||||
@if($workshop->ages)
|
||||
<div class="text-gray-600 text-sm mb-1 flex gap-2">
|
||||
<div class="w-6 flex items-center justify-center">
|
||||
<i class="fa-regular fa-face-smile"></i>
|
||||
</div>{{ isset($event->ages) && $event->ages !== '' ? 'Ages ' . $event->ages : 'All ages' }}
|
||||
</div>{{ $workshop->ages ? 'Ages ' . $workshop->ages : 'All ages' }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-gray-600 text-sm mb-1 flex gap-2">
|
||||
<div class="w-6 flex items-center justify-center">
|
||||
<i class="fa-solid fa-dollar-sign"></i>
|
||||
</div>{{ isset($event->price) && $event->price !== '' && $event->price !== '0' ? $event->price : 'Free' }}
|
||||
</div>
|
||||
{{ $workshop->price && $workshop->price !== '0' ? number_format((float)$workshop->price, 2) : 'Free' }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -4,16 +4,19 @@
|
||||
$colorClasses = [
|
||||
'outline' => 'hover:bg-gray-500 focus-visible:outline-primary-color text-gray-800 border border-gray-400 bg-white hover:text-white',
|
||||
'primary' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
||||
'primary-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
||||
'primary-outline' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||
'primary-outline-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||
'danger' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color bg-danger-color text-white',
|
||||
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white'
|
||||
'danger-outline' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color text-danger-color border border-danger-color bg-white hover:text-white',
|
||||
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white',
|
||||
'dark' => 'hover:bg-gray-900 focus-visible:outline-gray-800 bg-gray-800 text-white'
|
||||
][$color];
|
||||
$commonClasses = @twMerge(['whitespace-nowrap', 'text-center','justify-center','rounded-md','px-8','py-1.5','text-sm','font-semibold','leading-6','shadow-sm','focus-visible:outline','focus-visible:outline-2','focus-visible:outline-offset-2','transition'], ($class ?? ''));
|
||||
@endphp
|
||||
|
||||
@if($type == 'submit' || $type == 'button')
|
||||
@if($type === 'submit' || $type === 'button')
|
||||
<button type="{{ $type }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}>{{ $slot }}</button>
|
||||
@elseif($type == 'link')
|
||||
@elseif($type === 'link')
|
||||
<a href="{{ $href ?? '#' }}" target="{{ $target ?? '_self' }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}">{{ $slot }}</a>
|
||||
@endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@props(['type' => 'text', 'name', 'label' => '', 'value' => '', 'floating' => false, 'noLabel' => false, 'readonly' => false, 'info', 'error' => null, 'labelNotice' => null])
|
||||
@props(['type' => 'text', 'name', 'label' => '', 'value' => '', 'floating' => false, 'noLabel' => false, 'readonly' => false, 'info', 'error' => null, 'labelNotice' => null, 'placeholder' => '', 'fieldClasses' => '' ])
|
||||
|
||||
@php
|
||||
if($error === null) {
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
$hasError = $error !== '';
|
||||
$classes = 'disabled:bg-gray-100 bg-white block px-2.5 pb-2.5 w-full text-sm text-gray-900 rounded-lg border appearance-nonefocus:outline-none focus:ring-0 focus:border-blue-600 ' . ($hasError ? 'border-red-600 ring-red-600 focus:border-red-600 focus:ring-red-600' : 'border-gray-300 focus:border-indigo-300 focus:ring-indigo-300');
|
||||
$classes = 'disabled:bg-gray-100 bg-white block px-2.5 pb-2.5 w-full text-sm text-gray-900 rounded-lg border appearance-none focus:outline-none focus:ring-0 focus:border-blue-600 ' . ($hasError ? 'border-red-600 ring-red-600 focus:border-red-600 focus:ring-red-600' : 'border-gray-300 focus:border-indigo-300 focus:ring-indigo-300');
|
||||
$value = old($name, $value);
|
||||
@endphp
|
||||
|
||||
@@ -14,27 +14,27 @@
|
||||
@if($floating)
|
||||
<div class="relative">
|
||||
@if($type === 'textarea')
|
||||
<textarea class="{{ twMerge(['pt-4'], $classes) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
|
||||
<textarea class="{{ twMerge(['pt-4'], $classes, $attributes->get('fieldClasses')) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
|
||||
@else
|
||||
<input class="{{ twMerge(['pt-4'], $classes) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
<input class="{{ twMerge(['pt-4'], $classes, $attributes->get('fieldClasses')) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
@endif
|
||||
<label for="{{ $name }}" class="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto start-1">{{ $label }}</label>
|
||||
</div>
|
||||
@elseif($noLabel)
|
||||
<div class="relative">
|
||||
@if($type === 'textarea')
|
||||
<textarea class="{{ twMerge(['pt-2.5'], $classes) }}" name="{{ $name }}" placeholder="{{ $label }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
|
||||
<textarea class="{{ twMerge(['pt-2.5'], $classes, $fieldClasses) }}" name="{{ $name }}" placeholder="{{ $label }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
|
||||
@else
|
||||
<input class="{{ twMerge(['pt-2.5'], $classes) }}" autocomplete="off" placeholder="{{ $label }}" value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
<input class="{{ twMerge(['pt-2.5'], $classes, $fieldClasses) }}" autocomplete="off" placeholder="{{ $label }}" value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
<label for="{{ $name }}" class="block text-sm pl-1">{{ $label }}{!! isset($labelNotice) && $labelNotice !== '' ? '<i class="fa-solid fa-triangle-exclamation ml-1 text-gray-500 hover:text-black" data-tooltip="' . $labelNotice . '"></i>' : '' !!}</label>
|
||||
@if($type === 'textarea')
|
||||
<textarea class="{{ twMerge(['pt-2.5','mt-1','h-96'], $classes) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes->whereDoesntStartWith('x-') }}>{{ $value }}</textarea>
|
||||
<textarea class="{{ twMerge(['pt-2.5','mt-1','h-96'], $classes, $fieldClasses) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes->whereDoesntStartWith('x-') }}>{{ $value }}</textarea>
|
||||
@else
|
||||
<input class="{{ twMerge(['pt-2.5','mt-1'], $classes) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
<input class="{{ twMerge(['pt-2.5','mt-1'], $classes, $fieldClasses) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@endphp
|
||||
|
||||
<form method="GET" action="{{ url()->current() }}" class="{{ $attributes->get('class') }}">
|
||||
<div class="flex relative" x-data="{search:'{{request()->get('search')}}'}">
|
||||
<div class="flex relative" x-data="{search:'{{request()->get('q')}}'}">
|
||||
<input class="{{ $classes }}" autocomplete="off" placeholder="{{ $label }}" x-model="search" type="{{ $type }}" name="{{ $name }}" />
|
||||
<x-ui.button type="submit" class="rounded-l-none px-6"><i class="fa-solid fa-magnifying-glass"></i></x-ui.button>
|
||||
<i x-show="search" cloak class="absolute z-10 top-1/2 right-[4.5rem] transform -translate-y-1/2 text-gray-300 hover:text-gray-400 cursor-pointer fa-solid fa-circle-xmark" x-data x-on:click="search='';$nextTick(()=>{if('{{request()->get('search')}}'!==''){$el.closest('form').submit();}})"></i>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<p class="mb-2">We do not have a physical address as our workshops are delivered across Queensland. Visit the workshops page for each specific location.</p>
|
||||
<p class="mb-4">Official mail can be sent to the following postal address:</p>
|
||||
<p class="mb-2 text-center">STEMMechanics<br />
|
||||
1/4 Jordan Street<br />
|
||||
Edmonton, QLD, 4869<br />
|
||||
63 Dalton Street<br />
|
||||
Westcourt, QLD, 4870<br />
|
||||
Australia</p>
|
||||
<p class="mb-2 text-center"><strong class="font-semibold">ABN</strong>: 15 772 281 735</p>
|
||||
</x-container>
|
||||
|
||||
13
resources/views/emails/login-backup-code.blade.php
Normal file
13
resources/views/emails/login-backup-code.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that someone just logged in using a backup code.</p>
|
||||
<p>If this was you, then it is all good!</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email and resetting your backup codes by:</p>
|
||||
<ul>
|
||||
<li>Logging into your account on STEMMechanics</li>
|
||||
<li>Visit your account page</li>
|
||||
<li>Under <strong>Two Factor Authentication</strong> - Click <i>Reset Backup Codes</i></li>
|
||||
</ul>
|
||||
<p>Warm regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
8
resources/views/emails/login-tfa-disabled.blade.php
Normal file
8
resources/views/emails/login-tfa-disabled.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Disabled</strong>.</p>
|
||||
<p>If this was you, then it is all good! - Any previous <i>Backup TFA Codes</i> can no longer be used.</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email.</p>
|
||||
<p>Regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
8
resources/views/emails/login-tfa-enabled.blade.php
Normal file
8
resources/views/emails/login-tfa-enabled.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Enabled</strong>.</p>
|
||||
<p>If this was you, then it is all good!</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email.</p>
|
||||
<p>Regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
29
resources/views/emails/upcoming-workshops.blade.php
Normal file
29
resources/views/emails/upcoming-workshops.blade.php
Normal file
@@ -0,0 +1,29 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>Check out our exciting workshops coming up in the next few weeks:</p>
|
||||
<p class="center">
|
||||
@php
|
||||
$currentLocation = null;
|
||||
@endphp
|
||||
@foreach($workshops as $workshop)
|
||||
@if($workshop->location->name !== $currentLocation)
|
||||
<h2 style="margin-top: 32px; margin-bottom: 6px">{{ $workshop->location->name }}</h2>
|
||||
@php
|
||||
$currentLocation = $workshop->location->name;
|
||||
@endphp
|
||||
@endif
|
||||
<p style="margin-bottom: 6px">{{ $workshop->starts_at->format('D, j M, g:i A') . ' - ' }}<a href="{{ route('workshop.show', $workshop->slug) }}">{{ $workshop->title }}</a> ({{ ($workshop->price && is_numeric($workshop->price) && $workshop->price != '0' ? '$' . number_format((float)$workshop->price, 2) : 'Free') . ( $workshop->status === 'scheduled' ? ' / Opens soon' : '') }})</p>
|
||||
@endforeach
|
||||
<p class="tall center" style="margin-top: 32px">
|
||||
@component('mail::button', ['url' => 'https://stemmechanics.com.au/workshops'])
|
||||
View All Workshops
|
||||
@endcomponent
|
||||
</p>
|
||||
<p>We hope to see you at one of our upcoming workshops!</p>
|
||||
<p>Warm regards,</p>
|
||||
<p>— James 😁</p>
|
||||
@slot('subcopy')
|
||||
<h4>Why did I get this email?</h4>
|
||||
<p class="sub">You received this email as you are subscribed to our upcoming workshop email list. If you wish no longer receive this email, you can <a href="{{ $unsubscribeLink }}">unsubscribe here</a>.</p>
|
||||
@endslot
|
||||
@endcomponent
|
||||
@@ -1,12 +1,12 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
@component('mail::message', ['email' => $email, 'unsubscribe' => $unsubscribeLink])
|
||||
<p>Welcome to the community!</p>
|
||||
<p>Really glad to have you here and can't wait to see you at one of our workshops.</p>
|
||||
<p>You'll get information about upcoming workshops as it comes out.</p>
|
||||
<p>Even though this is (of course) an automated email, just wanted to say thanks for registering and intro myself.</p>
|
||||
<p>Even though this is (of course) an automated email, just wanted to say thanks and intro myself.</p>
|
||||
<p>If you didn't know, I'm James and I'm the founder of STEMMechanics. I promise not to spam you, sell your data, or send you anything I don't think is absolutely necessary.</p>
|
||||
<p>You know a bit about me but I don't know really anything about you...</p>
|
||||
<p><strong>If you're up for it</strong>, reply to this email and tell me a bit about yourself and also let me know what workshops you are interested in?</p>
|
||||
<p><strong>If you're up for it</strong>, reply to this email or join us in <a href="https://discord.gg/yNzk4x7mpD">Discord</a> and tell me a bit about yourself and also let me know what workshops you are interested in?</p>
|
||||
<p>I read and reply to every one 😁</p>
|
||||
<p>Talk soon</p>
|
||||
<p>—James</p>
|
||||
<p>— James</p>
|
||||
@endcomponent
|
||||
|
||||
5
resources/views/errors/401.blade.php
Normal file
5
resources/views/errors/401.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Unauthorized'))
|
||||
@section('code', '401')
|
||||
@section('message', __('Unauthorized'))
|
||||
5
resources/views/errors/402.blade.php
Normal file
5
resources/views/errors/402.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Payment Required'))
|
||||
@section('code', '402')
|
||||
@section('message', __('Payment Required'))
|
||||
5
resources/views/errors/419.blade.php
Normal file
5
resources/views/errors/419.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Page Expired'))
|
||||
@section('code', '419')
|
||||
@section('message', __('Page Expired'))
|
||||
5
resources/views/errors/429.blade.php
Normal file
5
resources/views/errors/429.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Too Many Requests'))
|
||||
@section('code', '429')
|
||||
@section('message', __('Too Many Requests'))
|
||||
5
resources/views/errors/500.blade.php
Normal file
5
resources/views/errors/500.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Server Error'))
|
||||
@section('code', '500')
|
||||
@section('message', __('Server Error'))
|
||||
53
resources/views/errors/layout.blade.php
Normal file
53
resources/views/errors/layout.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title')</title>
|
||||
|
||||
<!-- Styles -->
|
||||
<style>
|
||||
html, body {
|
||||
background-color: #fff;
|
||||
color: #636b6f;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-weight: 100;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.position-ref {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex-center position-ref full-height">
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
@yield('message')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
34
resources/views/errors/minimal.blade.php
Normal file
34
resources/views/errors/minimal.blade.php
Normal file
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
|
||||
<x-layout>
|
||||
<x-slot name="title">Workshops</x-slot>
|
||||
<x-mast>Workshops</x-mast>
|
||||
<section class="bg-gray-100">
|
||||
<x-container class="my-4">
|
||||
<x-ui.search class="md:hidden" name="search" label="Search" value="{{ request()->get('search') }}" />
|
||||
<form class="hidden md:flex gap-4" method="GET" action="{{ request()->url() }}">
|
||||
<x-ui.input no-label class="my-0 flex-1" type="text" name="search" label="Keywords" value="{{ request()->get('search') }}"/>
|
||||
<x-ui.input no-label class="my-0 flex-1" type="text" name="location" label="Location" value="{{ request()->get('location') }}"/>
|
||||
<x-ui.input no-label class="my-0 flex-1" type="text" name="date" label="Date Range" value="{{ request()->get('date') }}"/>
|
||||
<x-ui.button type="submit"><i class="fa-solid fa-magnifying-glass"></i></x-ui.button>
|
||||
</form>
|
||||
</x-container>
|
||||
|
||||
@if($events->isEmpty())
|
||||
<x-container class="mt-8">
|
||||
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
|
||||
</x-container>
|
||||
@else
|
||||
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach ($events as $event)
|
||||
<x-panel-event :event="$event" />
|
||||
@endforeach
|
||||
</x-container>
|
||||
<x-container>
|
||||
{{ $events->appends(request()->query())->links() }}
|
||||
</x-container>
|
||||
@endif
|
||||
</section>
|
||||
</x-layout>
|
||||
@@ -1,64 +0,0 @@
|
||||
<x-layout>
|
||||
<x-container>
|
||||
<x-ui.image-hero :image="$event->hero?->url" class="my-8" />
|
||||
<div class="flex sm:gap-16 gap-4 flex-col sm:flex-row">
|
||||
<div class="flex flex-col flex-1">
|
||||
<h1 class="text-3xl font-bold mb-6">{!! $event->title !!}</h1>
|
||||
<article class="content mb-4">{!! $event->content !!}</article>
|
||||
<x-ui.filelist class="mt-16" value="{!! $event->files()->orderBy('name')->get() !!}" />
|
||||
</div>
|
||||
<div class="flex flex-col sm:pt-8 basis-64 flex-grow-0 flex-shrink-0">
|
||||
@if($event->status === 'closed')
|
||||
<div class="sm-registration-closed">Registration for this event has closed.</div>
|
||||
@elseif($event->status === 'full')
|
||||
<div class="sm-registration-full">This workshop is currently full.</div>
|
||||
@elseif($event->status === 'private')
|
||||
<div class="sm-registration-private">This is a private event. Please contact the organiser for details.</div>
|
||||
@elseif($event->status === 'scheduled')
|
||||
<div class="sm-registration-scheduled">Registration for this workshop will open soon.</div>
|
||||
@elseif($event->status === 'cancelled')
|
||||
<div class="sm-registration-cancelled">This workshop has been cancelled.</div>
|
||||
@elseif($event->registration === 'none')
|
||||
<div class="sm-registration-none">Registration not required for this event. Arrive early to avoid disappointment as seating maybe limited.</div>
|
||||
@elseif($event->registration === 'link')
|
||||
<x-ui.button href="{{ $event->registration_data }}" class="my-4">Register for Event</x-ui.button>
|
||||
@elseif($event->registration === 'email')
|
||||
<div class="sm-registration-email">Registration for this event by emailing <a href="mailto:{{ $event->registration_data }}" class="link">{{ $event->registration_data }}</a>.</div>
|
||||
@elseif($event->registration === 'message')
|
||||
<div class="sm-registration-message">{{ $event->registration_data }}</div>
|
||||
@endif
|
||||
@if(auth()->user()?->admin)
|
||||
<x-ui.button class="mb-4" color="primary-outline" href="{{ route('admin.event.edit', $event) }}">Edit Workshop</x-ui.button>
|
||||
@endif
|
||||
<h2 class="text-gray-600 text-lg font-bold mt-4 mb-2"><i class="mr-1 fa-regular fa-calendar"></i> Date/Time</h2>
|
||||
<p class="text-gray-600 text-sm pl-6 mb-6">{!! implode('<br />', \App\Helpers::createTimeDurationStr($event->starts_at, $event->ends_at)) !!}</p>
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-location-dot"></i> Location</h2>
|
||||
<div class="text-gray-600 text-sm pl-6 mb-6">
|
||||
@if($event->location->url)
|
||||
<a href="{{ $event->location->url }}" class="link">
|
||||
@endif
|
||||
<p>{{ $event->location->name }}</p>
|
||||
@if($event->location->url)
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($event->location->address_url)
|
||||
<a href="{{ $event->location->address_url }}" class="link" target="_blank">
|
||||
@endif
|
||||
<p class="text-xs">{{ $event->location->address }}</p>
|
||||
@if($event->location->address_url)
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-regular fa-face-smile"></i> {{ isset($event->ages) && $event->ages !== '' ? 'Ages ' . $event->ages : 'All ages' }}</h2>
|
||||
@if(\App\Helpers::isUnderAge($event->ages))
|
||||
<p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-yellow-400">Parental supervision may be required for children 8 years of age and under.</p>
|
||||
@endif
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-dollar-sign"></i> {{ isset($event->price) && $event->price !== '' && $event->price !== '0' ? $event->price : 'Free' }}</h2>
|
||||
{{-- @if(isset($event->price) && $event->price !== '' && $event->price !== '0' && strtolower($event->price) !== 'free')--}}
|
||||
{{-- <p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-green-500">Payment by cash or EFTPOS accepted. Please ensure correct change.</p>--}}
|
||||
{{-- @endif--}}
|
||||
</div>
|
||||
</div>
|
||||
</x-container>
|
||||
</x-layout>
|
||||
@@ -1,6 +1,6 @@
|
||||
<x-layout id="home">
|
||||
<x-slot name="title">Home</x-slot>
|
||||
<section id="banner" class="bg-center bg-no-repeat bg-cover" style="background-image:linear-gradient(to right, rgba(0,0,0,.7),rgba(0,0,0,.2)),url('/home-hero.webp')">
|
||||
<section id="banner" class="bg-center bg-no-repeat bg-cover" style="background-image:linear-gradient(to right, rgba(0,0,0,.7),rgba(0,0,0,.2)),url({{asset('home-hero.webp')}})">
|
||||
<x-container class="py-32 relative">
|
||||
<h2 class="text-3xl text-white font-bold mb-4">Join the fun!</h2>
|
||||
<p class="text-white max-w-[42rem] mb-3">To keep up with our ever-changing world, it's important to encourage and support a new generation of curious minds who love science, engineering, art, and leadership.</p>
|
||||
@@ -8,24 +8,38 @@
|
||||
<p class="absolute bottom-3 right-5 bg-black bg-opacity-75 text-white text-xs px-3 py-1 rounded">Steady Hand Game in Ravenshoe</p>
|
||||
</x-container>
|
||||
</section>
|
||||
<section id="news" class="py-12">
|
||||
<section id="events" class="py-12">
|
||||
<x-container>
|
||||
<h2 class="text-2xl font-bold mb-6">Latest Posts</h2>
|
||||
@if($posts->isEmpty())
|
||||
<x-none-found item="posts" message="No posts have been published at this time" title="" />
|
||||
<h2 class="text-2xl font-bold mb-6">Upcoming workshops</h2>
|
||||
@if($workshops->isEmpty())
|
||||
<x-none-found item="workshops" message="No workshops have been scheduled at this time" title="" />
|
||||
@else
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach($posts as $index => $post)
|
||||
<x-panel-post :post="$post" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
|
||||
@foreach($workshops as $index => $workshop)
|
||||
<x-panel-workshop :workshop="$workshop" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</x-container>
|
||||
</section>
|
||||
{{-- <section id="news" class="py-12">--}}
|
||||
{{-- <x-container>--}}
|
||||
{{-- <h2 class="text-2xl font-bold mb-6">Latest Posts</h2>--}}
|
||||
{{-- @if($posts->isEmpty())--}}
|
||||
{{-- <x-none-found item="posts" message="No posts have been published at this time" title="" />--}}
|
||||
{{-- @else--}}
|
||||
{{-- <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">--}}
|
||||
{{-- @foreach($posts as $index => $post)--}}
|
||||
{{-- <x-panel-post :post="$post" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />--}}
|
||||
{{-- @endforeach--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- </x-container>--}}
|
||||
{{-- </section>--}}
|
||||
<section id="skills">
|
||||
<x-container class="bg-gray-200 py-32 my-8" inner-class="flex flex-row gap-16">
|
||||
<x-container class="bg-gray-200 py-32" inner-class="flex flex-row gap-16">
|
||||
<div class="flex-1 min-h-72 hidden md:block">
|
||||
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url('/home-green-screen.webp')"></div>
|
||||
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url({{asset('home-green-screen.webp')}})"></div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 text-center">
|
||||
<h2 class="text-3xl mb-4 text-center md:text-left">Build skills while having a great time</h2>
|
||||
@@ -33,47 +47,33 @@
|
||||
<div class="self-center">
|
||||
<p class="mb-6 text-left">To keep up with our ever-changing world, it's important to encourage and support a new generation of curious minds who love science, engineering, art, and leadership.</p>
|
||||
<div class="flex flex-grow justify-center items-center">
|
||||
<x-ui.button color="success" href="{{ route('event.index') }}" class="font-normal">Explore Workshops</x-ui.button>
|
||||
<x-ui.button color="success" href="{{ route('workshop.index') }}" class="font-normal">Explore Workshops</x-ui.button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-8 hidden sm:block md:hidden">
|
||||
<div class="h-48 w-48 bg-no-repeat bg-center bg-cover rounded-full" style="background-image:url('/home-green-screen.webp')"></div>
|
||||
<div class="h-48 w-48 bg-no-repeat bg-center bg-cover rounded-full" style="background-image:url({{asset('home-green-screen.webp')}})"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-container>
|
||||
</section>
|
||||
<section id="events" class="pt-4 pb-8">
|
||||
<x-container>
|
||||
<h2 class="text-2xl font-bold mb-6">Upcoming workshops</h2>
|
||||
@if($events->isEmpty())
|
||||
<x-none-found item="workshops" message="No workshops have been scheduled at this time" title="" />
|
||||
@else
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach($events as $index => $event)
|
||||
<x-panel-event :event="$event" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</x-container>
|
||||
</section>
|
||||
<section id="minecraft" class="bg-center bg-no-repeat bg-cover" style="background-image:url('/home-minecraft.webp')">
|
||||
<section id="minecraft" class="bg-center bg-no-repeat bg-cover" style="background-image:url({{asset('home-minecraft.webp')}})">
|
||||
<x-container class="text-white py-32">
|
||||
<h2 class="text-3xl mb-4">Play Minecraft with us</h2>
|
||||
<p class="mb-4">We invite you to join us on our <a href="https://stemcraft.com.au/" class="link">Minecraft server</a> where you can participate in weekly challenges and mini-games.</p>
|
||||
<div class="mb-4 flex gap-4">
|
||||
<img src="/home-minecraft-edu.webp" class="h-12" />
|
||||
<img src="{{ asset('home-minecraft-edu.webp') }}" alt="Minecraft Education" class="h-12" />
|
||||
<p>We also run workshops on our minecraft server, both online and offline, where you ca learn to make it rain rabbits, or grow flowers wherever you walk!</p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<img src="/home-minecraft-address.webp" class="h-12" />
|
||||
<img src="{{ asset('home-minecraft-address.webp') }}" alt="play.stemcraft.com.au" class="h-12" />
|
||||
</div>
|
||||
</x-container>
|
||||
</section>
|
||||
<section id="support">
|
||||
<x-container class="bg-gray-200 py-32 -mb-12" inner-class="flex flex-row gap-16">
|
||||
<div class="hidden sm:block flex-1">
|
||||
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url('/home-discord.webp')"></div>
|
||||
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url({{ asset('home-discord.webp') }})"></div>
|
||||
</div>
|
||||
<div class="flex-1 text-center">
|
||||
<h2 class="text-3xl mb-4 text-left">And the support doesn't stop!</h2>
|
||||
@@ -85,4 +85,13 @@
|
||||
</div>
|
||||
</x-container>
|
||||
</section>
|
||||
<section id="subscribe">
|
||||
<x-container class="bg-primary-color-dark py-24 -mb-12" inner-class="flex justify-center">
|
||||
<div class="max-w-[52rem]">
|
||||
<h2 class="text-3xl mb-0 text-white">Want to know what’s coming up?</h2>
|
||||
<p class="mb-6 text-left text-white">Sign up and we’ll send you updates on new workshops, special sessions and what’s happening around STEMMechanics.</p>
|
||||
<livewire:email-subscribe />
|
||||
</div>
|
||||
</x-container>
|
||||
</section>
|
||||
</x-layout>
|
||||
|
||||
39
resources/views/livewire/email-subscribe.blade.php
Normal file
39
resources/views/livewire/email-subscribe.blade.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<div>
|
||||
<form wire:submit.prevent="subscribe" class="flex flex-row justify-center">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
wire:model.defer="trap"
|
||||
autocomplete="off"
|
||||
tabindex="-1"
|
||||
class="hidden"
|
||||
/>
|
||||
|
||||
<x-ui.input
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email"
|
||||
no-label
|
||||
wire:model.defer="email"
|
||||
class="m-0"
|
||||
field-classes="rounded-r-none sm:w-96"
|
||||
/>
|
||||
|
||||
{{-- Submit button --}}
|
||||
<x-ui.button color="dark" type="submit" class="rounded-l-none">
|
||||
Subscribe
|
||||
</x-ui.button>
|
||||
</form>
|
||||
|
||||
@if($message)
|
||||
@if($success)
|
||||
<p class="mt-4 text-sm text-green-600 mx-auto border-green-800 bg-green-100 py-1 px-4 w-fit">
|
||||
<i class="fa fa-check mr-2"></i>{{ $message }}
|
||||
</p>
|
||||
@else
|
||||
<p class="mt-4 text-sm text-red-600 mx-auto border-red-800 bg-red-100 py-1 px-4 w-fit">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i>{{ $message }}
|
||||
</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@@ -2,15 +2,9 @@
|
||||
<x-slot name="title">Blog</x-slot>
|
||||
<x-mast>Blog</x-mast>
|
||||
<section class="bg-gray-100">
|
||||
<x-container class="pt-4">
|
||||
<form method="GET" action="{{ request()->url() }}">
|
||||
<x-ui.search name="search" label="Search" value="{{ request()->get('search') }}" />
|
||||
</form>
|
||||
</x-container>
|
||||
|
||||
@if($posts->isEmpty())
|
||||
<x-container class="mt-8">
|
||||
<x-none-found item="posts" search="{{ request()->get('search') }}" />
|
||||
<x-none-found item="posts" />
|
||||
</x-container>
|
||||
@else
|
||||
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
|
||||
40
resources/views/search.blade.php
Normal file
40
resources/views/search.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<x-layout>
|
||||
<x-mast title="Search" description='Results for "{{ $search }}"' />
|
||||
<x-container>
|
||||
<section class="bg-gray-100">
|
||||
<h2 class="text-2xl font-bold my-6">Posts</h2>
|
||||
@if($posts->isEmpty())
|
||||
<x-container class="mt-8">
|
||||
<x-none-found item="posts" search="{{ request()->get('search') }}" />
|
||||
</x-container>
|
||||
@else
|
||||
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach ($posts as $post)
|
||||
<x-panel-post :post="$post" />
|
||||
@endforeach
|
||||
</x-container>
|
||||
<x-container>
|
||||
{{ $posts->appends(request()->except('post'))->links('', ['pageName' => 'post']) }}
|
||||
</x-container>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="bg-gray-100">
|
||||
<h2 class="text-2xl font-bold my-6">Workshops</h2>
|
||||
@if($workshops->isEmpty())
|
||||
<x-container class="mt-8">
|
||||
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
|
||||
</x-container>
|
||||
@else
|
||||
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach ($workshops as $workshop)
|
||||
<x-panel-workshop :workshop="$workshop" />
|
||||
@endforeach
|
||||
</x-container>
|
||||
<x-container>
|
||||
{{ $workshops->appends(request()->except('workshop'))->links('', ['pageName' => 'workshop']) }}
|
||||
</x-container>
|
||||
@endif
|
||||
</section>
|
||||
</x-container>
|
||||
</x-layout>
|
||||
@@ -18,7 +18,7 @@
|
||||
<x-slot:footer>
|
||||
<x-mail::footer>
|
||||
<p>This email was sent to <a href="mailto:{{ $email }}">{{ $email }}</a><br />
|
||||
<a href="{{ route('index') }}">{{ config('app.name') }}</a> | 1/4 Jordan Street | Edmonton, QLD 4869 Australia<br />
|
||||
<a href="{{ route('index') }}">{{ config('app.name') }}</a> | 63 Dalton Street | Westcourt, QLD 4870 Australia<br />
|
||||
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}<br />
|
||||
<a href="{{ route('privacy') }}">Privacy Policy</a> | <a href="{{ route('terms-conditions') }}">Terms & Conditions</a> @isset($unsubscribe) | <a href="{{ $unsubscribe }}">Unsubscribe</a>@endisset
|
||||
</p>
|
||||
|
||||
@@ -43,7 +43,6 @@ h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@@ -68,7 +67,7 @@ h4 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
p, li {
|
||||
font-size: 14px;
|
||||
line-height: 1.2em;
|
||||
margin-top: 0;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
This email was sent to {{ $email }}
|
||||
|
||||
STEMMechanics | 1/4 Jordan Street | Edmonton, QLD 4869 Australia
|
||||
STEMMechanics | 63 Dalton Street | Westcourt, QLD 4870 Australia
|
||||
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}
|
||||
|
||||
@isset($unsubscribe) Unsubscribe: {{ $unsubscribe }}@endisset
|
||||
|
||||
58
resources/views/vendor/notifications/email.blade.php
vendored
Normal file
58
resources/views/vendor/notifications/email.blade.php
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
<x-mail::message>
|
||||
{{-- Greeting --}}
|
||||
@if (! empty($greeting))
|
||||
# {{ $greeting }}
|
||||
@else
|
||||
@if ($level === 'error')
|
||||
# @lang('Whoops!')
|
||||
@else
|
||||
# @lang('Hello!')
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Intro Lines --}}
|
||||
@foreach ($introLines as $line)
|
||||
{{ $line }}
|
||||
|
||||
@endforeach
|
||||
|
||||
{{-- Action Button --}}
|
||||
@isset($actionText)
|
||||
<?php
|
||||
$color = match ($level) {
|
||||
'success', 'error' => $level,
|
||||
default => 'primary',
|
||||
};
|
||||
?>
|
||||
<x-mail::button :url="$actionUrl" :color="$color">
|
||||
{{ $actionText }}
|
||||
</x-mail::button>
|
||||
@endisset
|
||||
|
||||
{{-- Outro Lines --}}
|
||||
@foreach ($outroLines as $line)
|
||||
{{ $line }}
|
||||
|
||||
@endforeach
|
||||
|
||||
{{-- Salutation --}}
|
||||
@if (! empty($salutation))
|
||||
{{ $salutation }}
|
||||
@else
|
||||
@lang('Regards'),<br>
|
||||
{{ config('app.name') }}
|
||||
@endif
|
||||
|
||||
{{-- Subcopy --}}
|
||||
@isset($actionText)
|
||||
<x-slot:subcopy>
|
||||
@lang(
|
||||
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n".
|
||||
'into your web browser:',
|
||||
[
|
||||
'actionText' => $actionText,
|
||||
]
|
||||
) <span class="break-all">[{{ $displayableActionUrl }}]({{ $actionUrl }})</span>
|
||||
</x-slot:subcopy>
|
||||
@endisset
|
||||
</x-mail::message>
|
||||
23
resources/views/workshop/index.blade.php
Normal file
23
resources/views/workshop/index.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<x-layout>
|
||||
<x-slot name="title">Workshops</x-slot>
|
||||
<x-mast title="Workshops" :tabs="[
|
||||
['title' => 'Upcoming', 'route' => route('workshop.index')],
|
||||
['title' => 'Past', 'route' => route('workshop.past.index')],
|
||||
]"/>
|
||||
<section class="bg-gray-100">
|
||||
@if($workshops->isEmpty())
|
||||
<x-container class="mt-8">
|
||||
<x-none-found item="workshops" />
|
||||
</x-container>
|
||||
@else
|
||||
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
|
||||
@foreach ($workshops as $workshop)
|
||||
<x-panel-workshop :workshop="$workshop" />
|
||||
@endforeach
|
||||
</x-container>
|
||||
<x-container>
|
||||
{{ $workshops->appends(request()->query())->links() }}
|
||||
</x-container>
|
||||
@endif
|
||||
</section>
|
||||
</x-layout>
|
||||
69
resources/views/workshop/show.blade.php
Normal file
69
resources/views/workshop/show.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<x-layout>
|
||||
<x-container>
|
||||
<x-ui.image-hero :image="$workshop->hero?->url" class="my-8" />
|
||||
<div class="flex sm:gap-16 gap-4 flex-col sm:flex-row">
|
||||
<div class="flex flex-col flex-1">
|
||||
<h1 class="text-3xl font-bold mb-6">{!! $workshop->title !!}</h1>
|
||||
<article class="content mb-4">{!! $workshop->content !!}</article>
|
||||
<x-ui.filelist class="mt-16" value="{!! $workshop->files()->orderBy('name')->get() !!}" />
|
||||
</div>
|
||||
<div class="flex flex-col sm:pt-8 basis-64 flex-grow-0 flex-shrink-0">
|
||||
@if($workshop->status === 'closed')
|
||||
<div class="sm-registration-closed">Registration for this event has closed.</div>
|
||||
@elseif($workshop->status === 'full')
|
||||
<div class="sm-registration-full">This workshop is currently full.</div>
|
||||
@elseif($workshop->status === 'private')
|
||||
<div class="sm-registration-private">This is a private event. Please contact the organiser for details.</div>
|
||||
@if($workshop->registration === 'link')
|
||||
<x-ui.button href="{!! $workshop->registration_data !!}" class="my-4">Register for Event</x-ui.button>
|
||||
@elseif($workshop->registration === 'message')
|
||||
<div class="sm-registration-message">{{ $workshop->registration_data }}</div>
|
||||
@endif
|
||||
@elseif($workshop->status === 'scheduled')
|
||||
<div class="sm-registration-scheduled">Registration for this workshop will open soon.</div>
|
||||
@elseif($workshop->status === 'cancelled')
|
||||
<div class="sm-registration-cancelled">This workshop has been cancelled.</div>
|
||||
@elseif($workshop->registration === 'none')
|
||||
<div class="sm-registration-none">Registration not required for this event. Arrive early to avoid disappointment as seating maybe limited.</div>
|
||||
@elseif($workshop->registration === 'link')
|
||||
<x-ui.button href="{!! $workshop->registration_data !!}" class="my-4">Register for Event</x-ui.button>
|
||||
@elseif($workshop->registration === 'email')
|
||||
<div class="sm-registration-email">Registration for this event by emailing <a href="mailto:{{ $workshop->registration_data }}" class="link">{{ $workshop->registration_data }}</a>.</div>
|
||||
@elseif($workshop->registration === 'message')
|
||||
<div class="sm-registration-message">{{ $workshop->registration_data }}</div>
|
||||
@endif
|
||||
@if(auth()->user()?->admin)
|
||||
<x-ui.button class="mb-4" color="primary-outline" href="{{ route('admin.workshop.edit', $workshop) }}">Edit Workshop</x-ui.button>
|
||||
@endif
|
||||
<h2 class="text-gray-600 text-lg font-bold mt-4 mb-2"><i class="mr-1 fa-regular fa-calendar"></i> Date/Time</h2>
|
||||
<p class="text-gray-600 text-sm pl-6 mb-6">{!! implode('<br />', \App\Helpers::createTimeDurationStr($workshop->starts_at, $workshop->ends_at)) !!}</p>
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-location-dot"></i> Location</h2>
|
||||
<div class="text-gray-600 text-sm pl-6 mb-6">
|
||||
@if($workshop->location->url)
|
||||
<a href="{{ $workshop->location->url }}" class="link">
|
||||
@endif
|
||||
<p>{{ $workshop->location->name }}</p>
|
||||
@if($workshop->location->url)
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($workshop->location->address_url)
|
||||
<a href="{{ $workshop->location->address_url }}" class="link" target="_blank">
|
||||
@endif
|
||||
<p class="text-xs">{{ $workshop->location->address }}</p>
|
||||
@if($workshop->location->address_url)
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-regular fa-face-smile"></i> {{ isset($workshop->ages) && $workshop->ages !== '' ? 'Ages ' . $workshop->ages : 'All ages' }}</h2>
|
||||
@if(\App\Helpers::isUnderAge($workshop->ages))
|
||||
<p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-yellow-400">Parental supervision may be required for children 8 years of age and under.</p>
|
||||
@endif
|
||||
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-dollar-sign"></i> {{ isset($workshop->price) && $workshop->price !== '' && $workshop->price !== '0' ? $workshop->price : 'Free' }}</h2>
|
||||
{{-- @if(isset($workshop->price) && $workshop->price !== '' && $workshop->price !== '0' && strtolower($workshop->price) !== 'free')--}}
|
||||
{{-- <p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-green-500">Payment by cash or EFTPOS accepted. Please ensure correct change.</p>--}}
|
||||
{{-- @endif--}}
|
||||
</div>
|
||||
</div>
|
||||
</x-container>
|
||||
</x-layout>
|
||||
@@ -1,9 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\SendEmail;
|
||||
use App\Mail\UpcomingWorkshops;
|
||||
use App\Models\Media;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* The scheduler is run from a cronjob on the server every minute.
|
||||
* To access the cronjob, run `crontab -u www-data -e` and add the following line:
|
||||
* * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
|
||||
*/
|
||||
|
||||
Artisan::command('email:send', function() {
|
||||
$subjects = [
|
||||
'🚀 Your STEM Adventure Awaits!',
|
||||
'⚡ Spark Your STEM Skills in a Workshop',
|
||||
'🔬 Unleash Your Curiosity in a Workshop',
|
||||
'🧠 Boost Your Brain with STEM Workshops',
|
||||
'🌟 Become a STEM Star: Join Our Workshops',
|
||||
'🔧 Tinker, Create, Learn in a Workshop',
|
||||
'🎨 Where Science Meets Creativity',
|
||||
'🏆 Level Up Your STEM Skills',
|
||||
'🌈 Discover the STEM Spectrum',
|
||||
'🔮 Future Innovators: Workshops Unveiled',
|
||||
];
|
||||
|
||||
$subject = $subjects[array_rand($subjects)];
|
||||
|
||||
$subscribers = DB::table('email_subscriptions')
|
||||
->whereNotNull('confirmed')
|
||||
->get();
|
||||
|
||||
foreach ($subscribers as $subscriber) {
|
||||
dispatch(new SendEmail($subscriber->email, new UpcomingWorkshops($subscriber->email, $subject)))->onQueue('mail');
|
||||
}
|
||||
})->purpose('Send newsletter to confirmed subscribers')->weeklyOn(3, '16:00');
|
||||
|
||||
Artisan::command('cleanup', function() {
|
||||
|
||||
@@ -19,13 +51,13 @@ Artisan::command('cleanup', function() {
|
||||
->update(['status' => 'published']);
|
||||
|
||||
// Open scheduled workshops
|
||||
DB::table('events')
|
||||
DB::table('workshops')
|
||||
->where('status', 'scheduled')
|
||||
->where('publish_at', '<', now())
|
||||
->update(['status' => 'open']);
|
||||
|
||||
// Close workshops
|
||||
DB::table('events')
|
||||
DB::table('workshops')
|
||||
->whereIn('status', ['open', 'full', 'private'])
|
||||
->where('closes_at', '<', now())
|
||||
->update(['status' => 'closed']);
|
||||
|
||||
@@ -6,24 +6,32 @@ use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\LocationController;
|
||||
use App\Http\Controllers\MediaController;
|
||||
use App\Http\Controllers\PostController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
use App\Http\Controllers\SubscribeController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use App\Http\Controllers\EventController;
|
||||
use App\Http\Controllers\WorkshopController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [HomeController::class, 'index'])->name('index');
|
||||
|
||||
Route::redirect('/events', '/workshops', 301);
|
||||
Route::redirect('/events/{event}', '/workshops/{event}', 301);
|
||||
//Route::get('posts', [PostController::class, 'index'])->name('post.index');
|
||||
//Route::get('posts/{post}', [PostController::class, 'show'])->name('post.show');
|
||||
Route::get('workshops', [WorkshopController::class, 'index'])->name('workshop.index');
|
||||
Route::get('workshops/past', [WorkshopController::class, 'past_index'])->name('workshop.past.index');
|
||||
Route::get('workshops/{workshop}', [WorkshopController::class, 'show'])->name('workshop.show');
|
||||
|
||||
Route::get('/posts', [PostController::class, 'index'])->name('post.index');
|
||||
Route::get('/posts/{post}', [PostController::class, 'show'])->name('post.show');
|
||||
Route::get('/workshops', [EventController::class, 'index'])->name('event.index');
|
||||
Route::get('/workshops/{event}', [EventController::class, 'show'])->name('event.show');
|
||||
Route::get('search', [SearchController::class, 'index'])->name('search.index');
|
||||
Route::get('unsubscribe/{email}', [SubscribeController::class, 'destroy'])->name('unsubscribe');
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::get('/account', [AccountController::class, 'show'])->name('account.show');
|
||||
Route::post('/account', [AccountController::class, 'update'])->name('account.update');
|
||||
Route::delete('/account', [AccountController::class, 'destroy'])->name('account.destroy');
|
||||
Route::get('/account/2fa', [AccountController::class, 'show_tfa'])->name('account.show.tfa');
|
||||
Route::get('/account/2fa/image', [AccountController::class, 'show_tfa_image'])->name('account.show.tfa.image');
|
||||
Route::post('/account/2fa', [AccountController::class, 'post_tfa'])->name('account.post.tfa');
|
||||
Route::post('/account/2fa/reset-backup-codes', [AccountController::class, 'post_tfa_reset_backup_codes'])->name('account.post.tfa.reset-backup-codes');
|
||||
Route::delete('/account/2fa', [AccountController::class, 'destroy_tfa'])->name('account.destroy.tfa');
|
||||
});
|
||||
|
||||
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
|
||||
@@ -33,6 +41,10 @@ Route::get('/register', [AuthController::class, 'showRegister'])->name('register
|
||||
Route::post('/register', [AuthController::class, 'postRegister'])->name('register.store');
|
||||
Route::get('/update-email', [AuthController::class, 'updateEmail'])->name('update.email');
|
||||
|
||||
Route::get('/about', function () {
|
||||
return view('about');
|
||||
})->name('about');
|
||||
|
||||
Route::get('/contact', function () {
|
||||
return view('contact');
|
||||
})->name('contact');
|
||||
@@ -67,12 +79,12 @@ Route::middleware('admin')->group(function () {
|
||||
Route::put('/admin/locations/{location}', [LocationController::class, 'update'])->name('admin.location.update');
|
||||
Route::delete('/admin/locations/{location}', [LocationController::class, 'destroy'])->name('admin.location.destroy');
|
||||
|
||||
Route::get('/admin/posts', [PostController::class, 'admin_index'])->name('admin.post.index');
|
||||
Route::get('/admin/posts/create', [PostController::class, 'admin_create'])->name('admin.post.create');
|
||||
Route::post('/admin/posts', [PostController::class, 'admin_store'])->name('admin.post.store');
|
||||
Route::get('/admin/posts/{post}', [PostController::class, 'admin_edit'])->name('admin.post.edit');
|
||||
Route::put('/admin/posts/{post}', [PostController::class, 'admin_update'])->name('admin.post.update');
|
||||
Route::delete('/admin/posts/{post}', [PostController::class, 'admin_destroy'])->name('admin.post.destroy');
|
||||
// Route::get('/admin/posts', [PostController::class, 'admin_index'])->name('admin.post.index');
|
||||
// Route::get('/admin/posts/create', [PostController::class, 'admin_create'])->name('admin.post.create');
|
||||
// Route::post('/admin/posts', [PostController::class, 'admin_store'])->name('admin.post.store');
|
||||
// Route::get('/admin/posts/{post}', [PostController::class, 'admin_edit'])->name('admin.post.edit');
|
||||
// Route::put('/admin/posts/{post}', [PostController::class, 'admin_update'])->name('admin.post.update');
|
||||
// Route::delete('/admin/posts/{post}', [PostController::class, 'admin_destroy'])->name('admin.post.destroy');
|
||||
|
||||
Route::get('/admin/users', [UserController::class, 'index'])->name('admin.user.index');
|
||||
Route::get('/admin/users/create', [UserController::class, 'create'])->name('admin.user.create');
|
||||
@@ -81,11 +93,12 @@ Route::middleware('admin')->group(function () {
|
||||
Route::put('/admin/users/{user}', [UserController::class, 'update'])->name('admin.user.update');
|
||||
Route::delete('/admin/users/{user}', [UserController::class, 'destroy'])->name('admin.user.destroy');
|
||||
|
||||
Route::get('/admin/events', [EventController::class, 'admin_index'])->name('admin.event.index');
|
||||
Route::get('/admin/events/create', [EventController::class, 'admin_create'])->name('admin.event.create');
|
||||
Route::get('/admin/events/{event}/duplicate', [EventController::class, 'admin_duplicate'])->name('admin.event.duplicate');
|
||||
Route::post('/admin/events', [EventController::class, 'admin_store'])->name('admin.event.store');
|
||||
Route::get('/admin/events/{event}', [EventController::class, 'admin_edit'])->name('admin.event.edit');
|
||||
Route::put('/admin/events/{event}', [EventController::class, 'admin_update'])->name('admin.event.update');
|
||||
Route::delete('/admin/events/{event}', [EventController::class, 'admin_destroy'])->name('admin.event.destroy');
|
||||
Route::get('/admin/workshops', [WorkshopController::class, 'admin_index'])->name('admin.workshop.index');
|
||||
Route::get('/admin/workshops/create', [WorkshopController::class, 'admin_create'])->name('admin.workshop.create');
|
||||
Route::get('/admin/workshops/{workshop}/duplicate', [WorkshopController::class, 'admin_duplicate'])->name('admin.workshop.duplicate');
|
||||
Route::post('/admin/workshops', [WorkshopController::class, 'admin_store'])->name('admin.workshop.store');
|
||||
Route::get('/admin/workshops/{workshop}', [WorkshopController::class, 'admin_edit'])->name('admin.workshop.edit');
|
||||
Route::put('/admin/workshops/{workshop}', [WorkshopController::class, 'admin_update'])->name('admin.workshop.update');
|
||||
Route::delete('/admin/workshops/{workshop}', [WorkshopController::class, 'admin_destroy'])->name('admin.workshop.destroy');
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user