Compare commits

..

No commits in common. "main" and "feature/push-notification-deals" have entirely different histories.

60 changed files with 187 additions and 2566 deletions

View File

@ -75,16 +75,4 @@ VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Same as the VAPID_PUBLIC_KEY
VITE_VAPID_PUBLIC_KEY="${VAPID_PUBLIC_KEY}"
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST=
REVERB_PORT=
REVERB_SCHEME=
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
VITE_VAPID_PUBLIC_KEY=BOBjjU2E-h8pDCV13yPwvMDR_WZwEhFmQY90gr16oJ5L1mpJ5qc7-0WzXcD1Z9D0Ozz0cLZxTe0_7nnDK3VFMP4

View File

@ -79,22 +79,11 @@ ## ⚙️ Installation
```bash
git clone https://git.sentientgeeks.us/joydip.manna/dealhub.git
cd dealhub
# Install dependencies
composer install
npm install
# Set VAPID for webpush
php artisan webpush:vapid
# Install Reverb
php artisan install:broadcasting
npm run dev
php artisan migrate
php artisan serve
# Start Reverb Server ( Another terminal )
php artisan reverb:start
```
---

View File

@ -1,4 +0,0 @@
- [ ] Live Support.
- [ ] Add an Audit Log table to save all actions.
- [ ] Link foreign keys properly with related tables in the database.
- [ ] Add debouncing to the search feature and create proper database indexes to improve search performance.

View File

@ -1,31 +0,0 @@
<?php
namespace App\Actions;
use App\Models\Inbox;
use App\Models\User;
use DB;
final readonly class CreateOrGetInboxAction
{
/**
* @throws \Throwable
*/
public function execute(User $recipient, User $sender): Inbox
{
$existingInbox = Inbox::whereHas('users', fn ($q) => $q->where('users.id', $sender->id))
->whereHas('users', fn ($q) => $q->where('users.id', $recipient->id))
->first();
if ($existingInbox) {
return $existingInbox;
}
return DB::transaction(function () use ($sender, $recipient) {
$inbox = Inbox::create();
$inbox->users()->attach([$sender->id, $recipient->id]);
return $inbox;
}, 2);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Actions;
use App\Events\MessageSent;
use App\Exceptions\MessageNotSendException;
use App\Models\User;
use DB;
final readonly class SendMessageAction
{
public function __construct(private CreateOrGetInboxAction $inboxAction) {}
/**
* @throws \Throwable
*/
public function execute(User $sender, User $recipient, array $data): void
{
try {
// find the inbox between the two users
DB::beginTransaction();
$inbox = $this->inboxAction->execute($recipient, $sender);
// update the inbox with the last message and last user as current user
$inbox->last_message = $data['message'];
$inbox->last_user_id = $sender->id;
$inbox->save();
// create a new message in the inbox
$message = $inbox->messages()->create([
'message' => $data['message'],
'user_id' => $sender->id,
]);
// Send the message to all other users in the inbox
broadcast(new MessageSent($message))->toOthers();
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw new MessageNotSendException('Message not sent.', previous: $e);
}
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Message $message)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
$users = $this->message->inbox->users->pluck('id')->toArray();
sort($users);
return [
new PrivateChannel("chat.{$users[0]}.{$users[1]}"),
];
}
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class MessageNotSendException extends Exception {}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\CreateOrGetInboxAction;
use App\Actions\SendMessageAction;
use App\Exceptions\MessageNotSendException;
use App\Http\Requests\SendMessageRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Log;
use Throwable;
class ChatController extends Controller
{
//
public function index(#[CurrentUser] User $user)
{
return view('dashboards.user.chat')
->with('inboxes', $user->inboxes);
}
public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetInboxAction $action)
{
try {
$inbox = $action->execute($recipient, $sender);
} catch (Throwable $e) {
Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
abort(500);
}
return view('dashboards.user.chat')
->with('recipient', $recipient)
->with('inboxes', $sender->inboxes)
->with('messages', $inbox->messages()->latest()->get());
}
/**
* @throws Throwable
*/
public function store(
#[CurrentUser] User $sender,
User $recipient,
SendMessageRequest $request,
SendMessageAction $action
) {
try {
$action->execute($sender, $recipient, $request->validated());
return response()->json(['message' => 'Message sent successfully.']);
} catch (MessageNotSendException $e) {
Log::error('Message send failed', [$e->getMessage()]);
return response()->json(['message' => 'Message sent failed.'], 500);
}
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserFollowedBroker
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$sender = \Auth::user();
if ($sender->isBroker()) {
return $next($request);
}
$recipientUser = $request->route('recipient');
if ($recipientUser->isBroker()) {
$recipient = $recipientUser->type;
$isFollowing = $recipient->followers->contains($sender->id);
if ($isFollowing) {
return $next($request);
}
abort(403, 'You are not following this broker.');
}
abort('404', 'Broker not found.');
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendMessageRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => 'required|string',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -14,9 +14,6 @@
* @property bool $verified
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Follow|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Customer> $followers
* @property-read int|null $followers_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker newModelQuery()

View File

@ -5,28 +5,6 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $text
* @property int $deal_id
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Deal|null $deal
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereDealId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereText($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUserId($value)
*
* @mixin \Eloquent
*/
class Comment extends Model
{
protected $fillable = [

View File

@ -13,8 +13,6 @@
* @property string|null $phone
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Follow> $followings
* @property-read int|null $followings_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer newModelQuery()

View File

@ -25,8 +25,6 @@
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $broker
* @property-read \App\Models\DealCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
@ -52,7 +50,6 @@
* @method static Builder<static>|Deal whereTitle($value)
* @method static Builder<static>|Deal whereUpdatedAt($value)
* @method static Builder<static>|Deal whereUserId($value)
* @method static Builder<static>|Deal withIsFollowedByCurrentUser()
* @method static Builder<static>|Deal withViewPerDeal()
*
* @mixin \Eloquent

View File

@ -5,26 +5,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $customer_id
* @property int $broker_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Broker|null $broker
* @property-read \App\Models\Customer|null $customer
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereBrokerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCustomerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Follow extends Pivot
{
protected $table = 'follows';

View File

@ -1,55 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string|null $last_message
* @property int|null $last_user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $lastUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read int|null $users_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastMessage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Inbox extends Model
{
protected $fillable = ['last_user_id', 'last_message'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function lastUser(): BelongsTo
{
return $this->belongsTo(User::class, 'id', 'last_user_id');
}
public function messages(): HasMany
{
return $this->hasMany(Message::class, 'inbox_id');
}
public function getRecipientAttribute(): User
{
// first user in the relationship that is NOT the authenticated user
return $this->users->where('id', '!=', auth()->id())->first();
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace App\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property int $user_id
* @property int $inbox_id
* @property string $message
* @property string|null $read_at
* @property string|null $delivered_at
* @property string|null $deleted_at
* @property string|null $failed_at
* @property Carbon $created_at
* @property-read Inbox|null $inbox
* @property-read User|null $user
*
* @method static Builder<static>|Message newModelQuery()
* @method static Builder<static>|Message newQuery()
* @method static Builder<static>|Message query()
* @method static Builder<static>|Message whereCreatedAt($value)
* @method static Builder<static>|Message whereDeletedAt($value)
* @method static Builder<static>|Message whereDeliveredAt($value)
* @method static Builder<static>|Message whereFailedAt($value)
* @method static Builder<static>|Message whereId($value)
* @method static Builder<static>|Message whereInboxId($value)
* @method static Builder<static>|Message whereMessage($value)
* @method static Builder<static>|Message whereReadAt($value)
* @method static Builder<static>|Message whereUserId($value)
*
* @mixin Eloquent
*/
class Message extends Model
{
public $timestamps = false;
protected $fillable = [
'inbox_id', 'user_id', 'message',
'read_at', 'deleted_at', 'failed_at',
'created_at', 'delivered_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function inbox(): BelongsTo
{
return $this->belongsTo(Inbox::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -6,20 +6,11 @@
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int|null $user_id
* @property string $page
* @property UserTypes|null $user_type
* @property string $created_at
* @property UserTypes $user_type
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit wherePage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserType($value)
*
* @mixin \Eloquent
*/

View File

@ -5,7 +5,6 @@
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@ -26,24 +25,16 @@
* @property string $role
* @property string|null $role_type
* @property int|null $role_id
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $dealsInteractions
* @property-read int|null $deals_interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Inbox> $inboxes
* @property-read int|null $inboxes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $interactedDeals
* @property-read int|null $interacted_deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, User> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \NotificationChannels\WebPush\PushSubscription> $pushSubscriptions
* @property-read int|null $push_subscriptions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\RecentSearch> $recentSearches
* @property-read int|null $recent_searches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
@ -167,14 +158,4 @@ public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function inboxes(): BelongsToMany
{
return $this->belongsToMany(Inbox::class);
}
public function messages(): HasMany
{
return $this->hasMany(Message::class);
}
}

View File

@ -8,7 +8,6 @@
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {

15
bootstrap/cache/packages.php vendored Executable file → Normal file
View File

@ -20,13 +20,6 @@
0 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
),
),
'laradumps/laradumps' =>
array (
'providers' =>
array (
0 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
),
),
'laravel-notification-channels/webpush' =>
array (
'providers' =>
@ -41,14 +34,6 @@
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/reverb' =>
array (
'providers' =>
array (
0 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
1 => 'Laravel\\Reverb\\ReverbServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>

46
bootstrap/cache/services.php vendored Executable file → Normal file
View File

@ -27,19 +27,16 @@
23 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
24 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
25 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
26 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
27 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
28 => 'Laravel\\Pail\\PailServiceProvider',
29 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
30 => 'Laravel\\Reverb\\ReverbServiceProvider',
31 => 'Laravel\\Sail\\SailServiceProvider',
32 => 'Laravel\\Tinker\\TinkerServiceProvider',
33 => 'Carbon\\Laravel\\ServiceProvider',
34 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
35 => 'Termwind\\Laravel\\TermwindServiceProvider',
36 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
37 => 'Pest\\Laravel\\PestServiceProvider',
38 => 'App\\Providers\\AppServiceProvider',
26 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
27 => 'Laravel\\Pail\\PailServiceProvider',
28 => 'Laravel\\Sail\\SailServiceProvider',
29 => 'Laravel\\Tinker\\TinkerServiceProvider',
30 => 'Carbon\\Laravel\\ServiceProvider',
31 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
32 => 'Termwind\\Laravel\\TermwindServiceProvider',
33 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
34 => 'Pest\\Laravel\\PestServiceProvider',
35 => 'App\\Providers\\AppServiceProvider',
),
'eager' =>
array (
@ -55,16 +52,14 @@
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
11 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
12 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
13 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
14 => 'Laravel\\Pail\\PailServiceProvider',
15 => 'Laravel\\Reverb\\ReverbServiceProvider',
16 => 'Carbon\\Laravel\\ServiceProvider',
17 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
18 => 'Termwind\\Laravel\\TermwindServiceProvider',
19 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
20 => 'Pest\\Laravel\\PestServiceProvider',
21 => 'App\\Providers\\AppServiceProvider',
12 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
13 => 'Laravel\\Pail\\PailServiceProvider',
14 => 'Carbon\\Laravel\\ServiceProvider',
15 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
16 => 'Termwind\\Laravel\\TermwindServiceProvider',
17 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
18 => 'Pest\\Laravel\\PestServiceProvider',
19 => 'App\\Providers\\AppServiceProvider',
),
'deferred' =>
array (
@ -226,8 +221,6 @@
'Barryvdh\\LaravelIdeHelper\\Console\\ModelsCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\MetaCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\EloquentCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Laravel\\Reverb\\ApplicationManager' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Reverb\\Contracts\\ApplicationProvider' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
@ -276,9 +269,6 @@
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
array (
),
'Laravel\\Reverb\\ApplicationManagerServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),

View File

@ -13,14 +13,12 @@
"blade-ui-kit/blade-heroicons": "^2.6",
"laravel-notification-channels/webpush": "^10.4",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.0",
"laravel/tinker": "^2.10.1",
"twilio/sdk": "^8.10"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.6",
"fakerphp/faker": "^1.23",
"laradumps/laradumps": "^5.0",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",

1296
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@ -1,95 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

View File

@ -1,32 +0,0 @@
<?php
use App\Models\User;
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('inboxes', function (Blueprint $table) {
$table->id()->index();
$table->text('last_message')->nullable();
$table->foreignIdFor(User::class, 'last_user_id')->nullable();
$table->timestamps();
$table->index(['last_user_id', 'created_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('inboxes');
}
};

View File

@ -1,36 +0,0 @@
<?php
use App\Models\Inbox;
use App\Models\User;
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('messages', function (Blueprint $table) {
$table->id()->index();
$table->foreignIdFor(User::class)->index();
$table->foreignIdFor(Inbox::class)->index();
$table->text('message');
$table->time('read_at')->nullable();
$table->time('delivered_at')->nullable();
$table->time('deleted_at')->nullable();
$table->time('failed_at')->nullable();
$table->timestamp('created_at')->useCurrent()->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('messages');
}
};

View File

@ -1,31 +0,0 @@
<?php
use App\Models\Inbox;
use App\Models\User;
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('inbox_user', function (Blueprint $table) {
$table->id()->index();
$table->foreignIdFor(Inbox::class)->index();
$table->foreignIdFor(User::class)->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('inbox_user');
}
};

159
package-lock.json generated
View File

@ -11,9 +11,7 @@
"@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.3.0",
"laravel-vite-plugin": "^2.0.0",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.0.0",
"vite": "^7.0.7"
}
@ -866,14 +864,6 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
@ -1327,25 +1317,6 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1388,32 +1359,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.4",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
@ -1751,20 +1696,6 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/laravel-echo": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.3.0.tgz",
"integrity": "sha512-wgHPnnBvfHmu2I58xJ4asZH37Nu6P0472ku6zuoGRLc3zEWwIbpovDLYTiOshDH1SM7rA6AjZTKuu+jYoM1tpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz",
@ -2089,14 +2020,6 @@
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -2172,16 +2095,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -2260,38 +2173,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2401,13 +2282,6 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
@ -2525,39 +2399,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -10,9 +10,7 @@
"@tailwindcss/vite": "^4.0.0",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.3.0",
"laravel-vite-plugin": "^2.0.0",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.0.0",
"vite": "^7.0.7"
},

View File

@ -15,7 +15,6 @@ import {deleteRecentSearch} from "./deleteRecentSearch.js";
import {initNavMenu} from "./nav-menu.js";
import {toggleShimmer} from "./shimmer.js";
import {follow} from "./interaction.js";
import {addMessageToChat, sendMessage} from "./message.js";
document.deleteSearch = deleteRecentSearch;
document.like = like;
@ -24,8 +23,7 @@ document.redirect = redirect;
document.showReportModal = showReportModal;
window.toggleShimmer = toggleShimmer;
window.follow = follow;
window.sendMessage = sendMessage;
window.addMessageToChat = addMessageToChat;
window.addEventListener('load', async () => {
const preloader = document.getElementById('preloader');
const content = document.getElementById('content');

View File

@ -9,11 +9,3 @@ window.Chart = Chart;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.common['Accept'] = 'application/json';
window.axios.defaults.headers.common['Content-Type'] = 'application/json';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allow your team to quickly build robust real-time web applications.
*/
import './echo';

View File

@ -97,7 +97,7 @@ function setDealDetails(dealDetails) {
async function setComments(dealId, dealModal) {
const commentsContainer = dealModal.querySelector('.comments-container');
toggleShimmer(false, commentsContainer);
commentsContainer.outerHTML = await getComments(dealId);
commentsContainer.innerHTML = await getComments(dealId);
toggleShimmer(true, commentsContainer);
}

View File

@ -1,14 +0,0 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

View File

@ -1,51 +0,0 @@
import {showToast} from "./toast.js";
export const sendMessage = async (element, event) => {
event.preventDefault();
const messageInput = element.querySelector('[name="message"]');
const message = messageInput.value;
if (!message) return;
const recipentId = element.dataset.recipientId;
messageInput.value = '';
// Capture the ID of the UI element we just added
const tempMessageId = addMessageToChat({message: message}, true);
try {
await axios.post(`/api/chat/${recipentId}/message`, {message: message});
} catch (e) {
console.error(e);
showToast('Message could not be sent.');
const failedMessage = document.getElementById(tempMessageId);
if (failedMessage) {
failedMessage.remove();
}
messageInput.value = message;
}
}
export const addMessageToChat = (message, right = false) => {
const chatContainer = document.getElementById('chat-container');
if (!chatContainer) return;
const tempId = 'msg-' + Date.now();
const placeholder = chatContainer.querySelector('#no-messages-placeholder');
if (placeholder) placeholder.remove();
const messagePlaceholder = `
<div id="${tempId}" class="grid px-4 my-1 w-full ${right ? 'place-items-end' : 'place-items-start'}">
<div class="max-w-[40vw] py-2 px-4 rounded-full ${right ? 'rounded-br-none' : 'rounded-tl-none'} bg-gray-200">
${message.message}
</div>
</div>`;
chatContainer.insertAdjacentHTML('afterbegin', messagePlaceholder);
chatContainer.scrollTop = 0;
// Return the ID so the caller can find this specific message later
return tempId;
}

View File

@ -1,17 +0,0 @@
<x-layout>
<div class="flex">
<x-dashboard.broker.sidebar.layout>
<div class="">
<div class="flex space-x-4 items-center border-b border-b-gray-300 pb-6 mb-4">
<x-logo/>
<p class="font-bold text-2xl whitespace-nowrap group-[.w-20]:hidden">Messages</p>
</div>
{{$sidebarItems ?? ''}}
</div>
</x-dashboard.broker.sidebar.layout>
<section
class="flex relative flex-col space-y-4 md:space-y-8 bg-[#F9FAFB] overflow-y-auto overflow-x-hidden h-screen w-full">
{{$slot}}
</section>
</div>
</x-layout>

View File

@ -1,58 +0,0 @@
@props(['recipient', 'messages' => []])
@php
$groupedMessages = $messages->groupBy(function($msg) {
return $msg->created_at->format('M j, Y');
});
@endphp
<x-dashboard.page-heading class="m-0 mb-0.5" :title="$recipient->name" description="offline"/>
<div class="bg-gray-50 h-full overflow-hidden flex-shrink-0">
<div id="chat-container" data-auth-id="{{ auth()->id() }}" data-partner-id="{{ $recipient->id }}"
class="text-sm flex flex-col-reverse overflow-y-scroll h-full max-h-screen pb-50 scroll-snap-y-container">
@forelse($groupedMessages as $date => $dayMessages)
@foreach($dayMessages as $message)
<x-chat.message :right="$message->user_id === auth()->id()">
<p>{{ $message->message }}</p>
<span class="text-[10px] opacity-70 block text-right mt-1">
{{ $message->created_at->format('g:i A') }}
</span>
</x-chat.message>
@endforeach
{{-- The Date Divider --}}
<div class="flex justify-center my-4 top-0">
<span class="bg-gray-200 text-gray-600 text-xs px-3 py-1 rounded-full shadow-sm">
{{ $date }}
</span>
</div>
@empty
<div id="no-messages-placeholder">No Messages Found !</div>
@endforelse
</div>
<x-chat.message-input :recipient_id="$recipient->id"/>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('chat-container');
if (container) {
const authId = parseInt(container.dataset.authId);
const partnerId = parseInt(container.dataset.partnerId);
// Sort IDs for consistent channel naming
const user1 = Math.min(authId, partnerId);
const user2 = Math.max(authId, partnerId);
window.Echo.private(`chat.${user1}.${user2}`)
.listen('MessageSent', (e) => {
const message = e.message;
if (!message) return;
// Check if user is the recipient of the message
if (message.user_id === partnerId) {
addMessageToChat({message: e.message.message}, false);
}
});
}
});
</script>

View File

@ -1,10 +0,0 @@
@props(['recipient_id'])
<div class="absolute bottom-5 w-8/12 rounded-xl left-50 p-2 bg-white border border-gray-300 shadow-xl">
<form id="messageBox" data-recipient-id="{{$recipient_id}}" onsubmit="sendMessage(this, event)" action=""
class="flex space-x-4 items-center">
<x-ui.textarea class="flex-1" rows="1" name="message" placeholder="Enter your message..."></x-ui.textarea>
<x-ui.button variant="neutral" icon="">
<x-heroicon-o-paper-airplane class="w-5 h-5"/>
</x-ui.button>
</form>
</div>

View File

@ -1,11 +0,0 @@
@props(['right' => false])
<div class="grid px-4 my-1 w-full @if($right) place-items-end @else place-items-start @endif">
<div class="max-w-[70vw] md:max-w-[40vw] py-2 px-4 bg-gray-200 rounded-3xl
@if($right)
rounded-br-none
@else
rounded-tl-none
@endif">
{{$slot}}
</div>
</div>

View File

@ -1,8 +0,0 @@
@props(['avatar', 'name', 'status' => 'inactive', 'message' => '', 'link' => ''])
<x-dashboard.broker.sidebar.item :link="$link" class="">
<x-ui.avatar class="w-12! h-12! text-xl!">{{$avatar}}</x-ui.avatar>
<div class="flex flex-col text-left max-w-24">
<p class="sidebar-text text-sm transition-opacity duration-300 ease-in-out font-bold">{{$name}}</p>
<p class="sidebar-text text-sm text-accent-600 transition-opacity duration-300 ease-in-out truncate">{{$message}}</p>
</div>
</x-dashboard.broker.sidebar.item>

View File

@ -6,7 +6,7 @@
<div id="sidebar"
class="flex flex-col p-4 pt-6 justify-between font-medium h-full w-full transition-all duration-300 ease-in-out">
<div>
<div class="flex space-x-4 border-b border-b-gray-300 pb-6 ">
<div class="flex space-x-4 border-b border-b-gray-300 pb-6 n">
<x-logo/>
<a href="{{route('home')}}" class="whitespace-nowrap group-[.w-20]:hidden">
<p class="text-2xl font-bold">DealHub</p>
@ -64,7 +64,7 @@ class="py-3 pl-3 border border-white hover:bg-red-50 hover:border-red-200 rounde
{{-- Toggle Button --}}
<div
class="text-gray-500 cursor-pointer hover:text-gray-900 rounded-full p-1.5 bg-white border border-gray-300 absolute -right-3.5 top-21 z-20">
class="text-gray-500 cursor-pointer hover:text-gray-900 rounded-full p-1.5 bg-white border border-gray-300 absolute -right-3.5 top-21">
<x-heroicon-c-chevron-left id="closeSidebarBtn" class="w-4"/>
<x-heroicon-c-chevron-right id="openSidebarBtn" class="w-4 hidden"/>
</div>

View File

@ -1,47 +1,61 @@
<x-dashboard.broker.sidebar.layout>
<div class="">
<div class="flex space-x-4 border-b border-b-gray-300 pb-6">
<x-logo/>
<a href="{{route('home')}}" class="whitespace-nowrap">
<p class="text-2xl font-bold">DealHub</p>
<p class="text-accent-600 text-sm">Broker Panel</p>
</a>
</div>
@props(['activeClass' => 'bg-gray-100 text-gray-900'])
<div class="pt-6 flex flex-col space-y-3 whitespace-nowrap">
<x-dashboard.broker.sidebar.item :link="route('broker.dashboard')">
<x-heroicon-o-squares-2x2 class="min-w-5 w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Dashboard</p>
</x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :link="route('broker.deals.create')">
<x-heroicon-o-plus class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Create Deals</p>
</x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :link="route('broker.deals.index')">
<x-heroicon-o-document-text class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">All Deals</p>
</x-dashboard.broker.sidebar.item>
</div>
</div>
<div class="">
<x-dashboard.broker.sidebar.item :link="route('broker.profile.show', auth()->user()->id)">
<x-heroicon-o-user class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">Profile</p>
</x-dashboard.broker.sidebar.item>
<form method="post" action="{{route('logout')}}">
@csrf
@method('delete')
<button
class="py-3 pl-3 border border-white hover:bg-red-50 hover:border-red-200 rounded-xl w-full mt-4 transition-all">
<div class="flex space-x-3 items-center text-red-500">
<x-heroicon-o-arrow-right-start-on-rectangle class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">Logout</p>
<div id="sidebarWrapper" {{$attributes->merge([ 'class' => 'border-r border-r-gray-300 transition-all duration-300 ease-in-out w-64 relative'])}}>
<div class="hidden md:flex h-screen items-center">
<div id="sidebar" class="flex flex-col p-4 pt-6 justify-between font-medium h-full w-full overflow-hidden transition-all duration-300 ease-in-out">
<div class="">
<div class="flex space-x-4 border-b border-b-gray-300 pb-6">
<x-logo/>
<a href="{{route('home')}}" class="whitespace-nowrap">
<p class="text-2xl font-bold">DealHub</p>
<p class="text-accent-600 text-sm">Broker Panel</p>
</a>
</div>
</button>
</form>
<div class="pt-6 flex flex-col space-y-3 whitespace-nowrap">
<x-dashboard.broker.sidebar.item :link="route('broker.dashboard')">
<x-heroicon-o-squares-2x2 class="min-w-5 w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Dashboard</p>
</x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :link="route('broker.deals.create')">
<x-heroicon-o-plus class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out ">Create Deals</p>
</x-dashboard.broker.sidebar.item>
<x-dashboard.broker.sidebar.item :link="route('broker.deals.index')">
<x-heroicon-o-document-text class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">All Deals</p>
</x-dashboard.broker.sidebar.item>
</div>
</div>
<div class="">
<x-dashboard.broker.sidebar.item :link="route('broker.profile.show', auth()->user()->id)">
<x-heroicon-o-user class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">Profile</p>
</x-dashboard.broker.sidebar.item>
<form method="post" action="{{route('logout')}}">
@csrf
@method('delete')
<button class="py-3 pl-3 border border-white hover:bg-red-50 hover:border-red-200 rounded-xl w-full mt-4 transition-all">
<div class="flex space-x-3 items-center text-red-500">
<x-heroicon-o-arrow-right-start-on-rectangle class="w-5 min-w-5"/>
<p class="sidebar-text transition-opacity duration-300 ease-in-out">Logout</p>
</div>
</button>
</form>
</div>
</div>
{{-- Toggle Button --}}
<div class="text-gray-500 cursor-pointer hover:text-gray-900 rounded-full p-1.5 bg-white border border-gray-300 absolute -right-3.5 top-21">
<x-heroicon-c-chevron-left id="closeSidebarBtn" class="w-4"/>
<x-heroicon-c-chevron-right id="openSidebarBtn" class="w-4 hidden"/>
</div>
</div>
</x-dashboard.broker.sidebar.layout>
</div>
@vite(['resources/js/sidebar.js'])

View File

@ -6,15 +6,9 @@
@endphp
@aware(['activeClass' => 'bg-gray-100 text-gray-900'])
<div class="relative group/item w-full hover:z-10">
@if(filled($link))
<a href="{{$link}}" {{$attributes->class(["flex space-x-3 items-center pl-3 py-3 rounded-xl hover:bg-gray-100 border border-transparent ease-in-out transition-all duration-300 active:scale-80 hover:border-gray-300", $activeClass => $active])}} >
{{$slot}}
</a>
@else
<button {{$attributes->class(["flex space-x-3 items-center pl-3 py-3 rounded-xl hover:bg-gray-100 border border-transparent ease-in-out transition-all duration-300 active:scale-80 hover:border-gray-300", $activeClass => $active])}} >
{{$slot}}
</button>
@endif
<a href="{{$link}}" {{$attributes->class(["flex space-x-3 items-center pl-3 py-3 rounded-xl hover:bg-gray-100 border border-transparent ease-in-out transition-all duration-300 active:scale-80 hover:border-gray-300", $activeClass => $active])}} >
{{$slot}}
</a>
@if($tooltip !== '')
<span
class="absolute z-10 top-[30%] hidden group-[.w-20]:group-hover/item:block left-[110%] py-1 px-2 rounded-lg bg-gray-900 text-xs whitespace-nowrap text-white">

View File

@ -1,19 +0,0 @@
@props(['activeClass' => 'bg-gray-100 text-gray-900'])
<div id="sidebarWrapper" {{$attributes->merge([ 'class' => 'border-r border-r-gray-300 transition-all group duration-300 ease-in-out w-64 relative z-10'])}}>
<div class="hidden md:flex w-max h-screen items-center">
<div id="sidebar" class="flex flex-col p-4 pt-6 justify-between font-medium h-full w-full overflow-hidden transition-all duration-300 ease-in-out">
{{$slot}}
</div>
{{-- Toggle Button --}}
<div class="text-gray-500 cursor-pointer hover:text-gray-900 rounded-full p-1.5 bg-white border border-gray-300 absolute -right-3.5 top-21 z-30">
<x-heroicon-c-chevron-left id="closeSidebarBtn" class="w-4"/>
<x-heroicon-c-chevron-right id="openSidebarBtn" class="w-4 hidden"/>
</div>
</div>
</div>
@push('scripts')
@vite(['resources/js/sidebar.js'])
@endpush

View File

@ -1,5 +1,5 @@
@props(['title' => '', 'description' => '', 'backLink' => ''])
<section {{$attributes->merge(['class' => 'flex space-x-6 items-center justify-between wrapper py-6 shadow w-full'])}}>
<section class="flex space-x-6 items-center justify-between wrapper py-6 shadow w-full">
<div class="flex space-x-6 items-center">
@if($backLink !== '')
<div class="">

View File

@ -1,15 +1,11 @@
@props(['broker', 'is_followed' => false])
@props(['broker' => '', 'is_followed' => false])
<div {{$attributes->merge(['class' => "p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl"])}}>
<div class="flex space-x-2 items-center mb-2">
<p class="font-bold">Broker Contact</p>
<x-ui.button-sm data-is-loading="false" data-followed="{{$is_followed ? 'true' : 'false'}}"
onclick="follow(this, {{$broker->role_id ?? 0}})" class="followBtn group p-0! mt-0.5">
<div class="flex space-x-4 items-baseline">
<p class="font-bold mb-2">Broker Contact</p>
<x-ui.button-sm data-is-loading="false" data-followed="{{$is_followed ? 'true' : 'false'}}" onclick="follow(this, {{$broker->role_id ?? ''}})" class="followBtn group p-0! mt-0.5">
<span class="group-data-[followed=true]:hidden text-blue-600">Follow</span>
<span class="group-data-[followed=false]:hidden text-accent-600">Unfollow</span>
</x-ui.button-sm>
<x-ui.button-sm :link="route('chat.show', ['recipient' => $broker->id ?? 0])">
<x-heroicon-o-chat-bubble-oval-left class="w-4 text-accent-600"/>
</x-ui.button-sm>
</div>
<div class="text-accent-600 space-y-1">
<p data-is-loading="false" class="broker-name">{{$broker->name ?? ''}}</p>

View File

@ -1,5 +1,5 @@
@props(['comments' => []])
<div data-is-loading="true" class="comments-container mt-2 space-y-2 max-h-40 overflow-y-auto data-[is-loading=true]:h-10">
<div data-is-loading="true" class="comments-container mt-2 space-y-2 max-h-40 overflow-y-scroll data-[is-loading=true]:h-10">
@forelse($comments as $comment)
<x-dashboard.user.deal-comment.item :comment="$comment"/>
@empty

View File

@ -1,4 +1,4 @@
<x-ui.modal id="deal-modal" class="deal-identifier w-11/12 md:w-10/12 overflow-y-auto">
<x-ui.modal id="deal-modal" class="deal-identifier w-11/12 md:w-10/12 overflow-scroll">
<form class="flex justify-between items-start mb-4" method="dialog">
<p class="text-xl font-bold">Deal Details</p>
<button type="submit" class="">

View File

@ -1,4 +1,4 @@
@props(['deal'])
@props(['deal' => '', 'broker' => ''])
<x-ui.image-card class="deal-identifier deal-card shadow-lg cursor-pointer" :image="asset('storage/'.$deal->image)"
data-deal-id="{{$deal->id}}">
<div class="bg-white pt-8 p-4 h-full space-y-2 flex flex-col justify-between">
@ -24,7 +24,7 @@
</a>
@endguest
@auth
<x-dashboard.user.broker-contact :broker="$deal->broker" :is_followed="$deal->is_followed"/>
<x-dashboard.user.broker-contact :broker="$broker" :is_followed="$deal->is_followed"/>
@endauth
<div class="flex justify-between items-center">

View File

@ -1,7 +1,7 @@
@props(['deals' => [], 'isInteractive'])
<div class="grid md:grid-cols-2 gap-6">
@forelse($deals as $deal)
<x-dashboard.user.listing-card :deal="$deal" />
<x-dashboard.user.listing-card :deal="$deal" :broker="$deal->broker"/>
@empty
<p class="col-span-2 text-sm text-center text-accent-600 mt-12">No Deals found !</p>
@endforelse

View File

@ -1,40 +1,31 @@
@php use App\Enums\UserTypes; @endphp
@props(['profileLink' => ''])
<div class="flex items-center">
@auth
<div class="relative group">
<x-ui.button icon="user-circle" class="cursor-pointer" onclick="showMenu(this)"></x-ui.button>
<ul class="menu opacity-0 z-10 scale-10 group-hover:scale-100 group-hover:opacity-100 transition-all duration-300 ease-in-out w-48 absolute right-0 bg-white border border-gray-300 rounded-md shadow-xl py-2 text-accent-600">
<div class="relative group">
<x-ui.button icon="user-circle" class="cursor-pointer" onclick="showMenu(this)"></x-ui.button>
<ul class="menu opacity-0 z-10 scale-10 group-hover:scale-100 group-hover:opacity-100 transition-all duration-300 ease-in-out w-48 absolute right-0 bg-white border border-gray-300 rounded-md shadow-xl py-2 text-accent-600">
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
<a href="{{$profileLink}}" class="flex space-x-4">
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
<x-heroicon-o-user class="w-4"/>
</div>
<p>Profile</p>
</a>
</li>
@if(auth()->check() && auth()->user()->role === \App\Enums\UserTypes::Broker->value)
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
<a href="{{$profileLink}}" class="flex space-x-4">
<a href="{{route('broker.dashboard')}}" class="flex space-x-4">
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
<x-heroicon-o-user class="w-4"/>
<x-heroicon-o-adjustments-horizontal class="w-4"/>
</div>
<p>Profile</p>
<p>Control Panel</p>
</a>
</li>
@endif
@if(auth()->check() && auth()->user()->role === UserTypes::Broker->value)
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
<a href="{{route('broker.dashboard')}}" class="flex space-x-4">
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
<x-heroicon-o-adjustments-horizontal class="w-4"/>
</div>
<p>Control Panel</p>
</a>
</li>
@endif
<li class="py-2 px-4 hover:bg-gray-100 hover:text-gray-900 hover:cursor-pointer hover:font-bold">
<a href="{{route('chat')}}" class="flex space-x-4">
<div class="p-1 bg-gray-200 rounded-xl text-gray-900">
<x-heroicon-o-chat-bubble-left-right class="w-4"/>
</div>
<p>Chat</p>
</a>
</li>
</ul>
</div>
</ul>
</div>
@endauth
@guest
<x-ui.button variant="neutral" link="{{route('login.create')}}">

View File

@ -1,4 +0,0 @@
<section
{{$attributes->merge(['class' => 'w-25 h-25 rounded-xl bg-linear-150 from-[#305afc] to-[#941dfb] text-5xl text-white flex justify-center items-center'])}}>
{{$slot}}
</section>

View File

@ -1,6 +1,7 @@
@props(['label' => '', 'name' => '', 'placeholder' => '', 'required' => false, 'value' => '', 'description', 'rows'=>1])
<div {{$attributes->merge(['class' => 'flex flex-col space-y-2'])}}>
@if($label !== '')
@props(['label' => '', 'name' => '', 'placeholder' => '', 'required' => false, 'value' => '', 'description'])
@if($label !== '')
<div class="flex flex-col space-y-2">
<label class="text-sm font-bold" for="{{$name}}">
{{$label}}
@ -8,17 +9,17 @@
*
@endif
</label>
@endif
<textarea
class="bg-[#F3F3F5] py-2 px-4 rounded-lg"
rows="{{$rows}}"
class="bg-[#F3F3F5] py-2 px-4 rounded-lg h-40"
name="{{$name}}" placeholder="{{$placeholder}}"
required="{{$required?'required':''}}"
>{{old($name, $value)}}</textarea>
@isset($description)
<p class="text-accent-600 text-xs">{{$description}}</p>
@endisset
@isset($description)
<p class="text-accent-600 text-xs">{{$description}}</p>
@endisset
<x-ui.inline-error :name="$name"/>
</div>
<x-ui.inline-error :name="$name"/>
</div>
@endif

View File

@ -23,7 +23,10 @@ class="border border-accent-600/40">
<div
class="col-span-8 place-self-start md:col-span-2 lg:col-span-1 flex items-center justify-center w-full">
<x-ui.avatar>{{$initials}}</x-ui.avatar>
<div
class="w-25 h-25 rounded-xl bg-linear-150 from-[#305afc] to-[#941dfb] text-5xl text-white flex justify-center items-center">
{{$initials}}
</div>
</div>
<div class="col-span-8 md:col-span-6 lg:col-span-7 flex flex-col space-y-6">

View File

@ -1,25 +0,0 @@
@php use App\Services\ProfileInitialsService; @endphp
<x-chat.layout>
<x-slot:sidebarItems>
@forelse($inboxes as $inbox)
<x-chat.sidebar-item
:name="$inbox->recipient->name"
:link="route('chat.show', $inbox->recipient->id)"
:avatar="(new ProfileInitialsService)->create($inbox->recipient->name)"
:message="$inbox->last_message"/>
@empty
No chats found !
@endforelse
</x-slot:sidebarItems>
<div class="overflow-y-hidden">
@if(isset($recipient))
<x-chat.message-box :recipient="$recipient" :messages="$messages"/>
@else
<div class="w-full h-full flex items-center justify-center">
<p class="font-bold text-5xl text-gray-400">Start a chat ! </p>
</div>
@endif
</div>
<x-ui.toast/>
</x-chat.layout>

View File

@ -1,6 +1,5 @@
<?php
use App\Http\Controllers\ChatController;
use App\Http\Controllers\RecentSearchController;
use Illuminate\Support\Facades\Route;
@ -13,6 +12,4 @@
include __DIR__.'/push-notification.php';
Route::delete('/recent-search/{recentSearch}', RecentSearchController::class)->name('recent-search.destroy');
Route::post('/chat/{recipient}/message', [ChatController::class, 'store'])->name('chat.message');
});

View File

@ -1,13 +0,0 @@
<?php
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('chat.{user1}.{user2}', function (User $user, int $user1, int $user2) {
// Only allow the user if their ID matches one of the two in the channel name
return $user->id === $user1 || $user->id === $user2;
});

View File

@ -10,14 +10,26 @@
require __DIR__.'/web/interaction.php';
require __DIR__.'/web/customer.php';
require __DIR__.'/web/admin.php';
require __DIR__.'/web/chat.php';
Route::get('/', HomeController::class)->name('home');
Route::get('/explore', ExplorePageController::class)->name('explore');
Route::post('/contact', ContactController::class)->name('contact');
Route::get('/test-openssl', function () {
$res = openssl_pkey_new([
'curve_name' => 'prime256v1',
'private_key_type' => OPENSSL_KEYTYPE_EC,
]);
if ($res === false) {
return 'ERROR: '.openssl_error_string(); // likely "error:02001003:system library:fopen:No such process"
}
return 'SUCCESS: OpenSSL is configured correctly.';
});
/**
* This route is accessed by JS XHR requests, and is loaded here cause
* This routes are accessed by JS XHR requests, and is loaded here cause
* we do not want to use sanctum for web requests
*/
// ------------- API Routes ------------

View File

@ -1,12 +0,0 @@
<?php
use App\Http\Controllers\ChatController;
use App\Http\Middleware\EnsureUserFollowedBroker;
Route::middleware('auth')->group(function () {
Route::get('/chat', [ChatController::class, 'index'])->name('chat');
Route::get('/chat/{recipient}', [ChatController::class, 'show'])
->middleware(EnsureUserFollowedBroker::class)
->name('chat.show');
});