feat: Realtime Functionality
continuous-integration/drone/push Build is passing Details

main
Flycro 2024-03-23 17:40:59 +01:00
parent d6ec298e56
commit 5f1e3ee176
20 changed files with 759 additions and 116 deletions

View File

@ -0,0 +1,42 @@
<?php
namespace App\Events\BookRecommendation;
use App\Models\BookRecommendation;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class BookRecommendationCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public BookRecommendation $bookRecommendation
)
{
//
}
public function broadcastAs(): string
{
return 'BookRecommendationCreated';
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('BookRecommendation'),
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Events\BookRecommendation;
use App\Models\BookRecommendation;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class BookRecommendationDeleted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public BookRecommendation $bookRecommendation
)
{
//
}
public function broadcastAs(): string
{
return 'BookRecommendationDeleted';
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('BookRecommendation'),
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Events\BookRecommendation;
use App\Models\BookRecommendation;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class BookRecommendationUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public BookRecommendation $bookRecommendation
)
{
//
}
public function broadcastAs(): string
{
return 'BookRecommendationUpdated';
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('BookRecommendation'),
];
}
}

View File

@ -2,7 +2,12 @@
namespace App\Models; namespace App\Models;
use App\Events\BookRecommendation\BookRecommendationCreated;
use App\Events\BookRecommendation\BookRecommendationDeleted;
use App\Events\BookRecommendation\BookRecommendationUpdated;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class BookRecommendation extends Model class BookRecommendation extends Model
{ {
@ -20,20 +25,35 @@ class BookRecommendation extends Model
'published_at', 'published_at',
]; ];
protected static function booted() :void
{
static::created(static function ($bookRecommendation) {
broadcast(new BookRecommendationCreated($bookRecommendation))->toOthers();
});
static::updated(static function ($task) {
broadcast(new BookRecommendationUpdated($task))->toOthers();
});
static::deleting(static function ($task) {
broadcast(new BookRecommendationDeleted($task))->toOthers();
});
}
/** /**
* Get the user that recommended the book. * Get the user that recommended the book.
*/ */
public function recommender() public function recommender(): BelongsTo
{ {
return $this->belongsTo(User::class, 'recommended_by'); return $this->belongsTo(User::class, 'recommended_by');
} }
public function votes() public function votes(): HasMany
{ {
return $this->hasMany(Vote::class); return $this->hasMany(Vote::class);
} }
public function deadlines() public function deadlines(): HasMany
{ {
return $this->hasMany(Deadline::class); return $this->hasMany(Deadline::class);
} }

View File

@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
api: __DIR__.'/../routes/api.php', api: __DIR__.'/../routes/api.php',
apiPrefix: '', apiPrefix: '',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {

View File

@ -18,6 +18,7 @@
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"league/flysystem-aws-s3-v3": "^3.24", "league/flysystem-aws-s3-v3": "^3.24",
"predis/predis": "*", "predis/predis": "*",
"pusher/pusher-php-server": "^7.2",
"spatie/laravel-permission": "^6.4" "spatie/laravel-permission": "^6.4"
}, },
"require-dev": { "require-dev": {

359
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ec31886b4063db8aa1efc22c187f614a", "content-hash": "572762c5d2b5b91e9a8012b47f3cd07f",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.301.1", "version": "3.301.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba" "reference": "18c0ebd71d3071304f1ea02aa9af75f95863177a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0a910d2b35e7087337cdf3569dc9b6ce232aafba", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/18c0ebd71d3071304f1ea02aa9af75f95863177a",
"reference": "0a910d2b35e7087337cdf3569dc9b6ce232aafba", "reference": "18c0ebd71d3071304f1ea02aa9af75f95863177a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -151,9 +151,9 @@
"support": { "support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.301.1" "source": "https://github.com/aws/aws-sdk-php/tree/3.301.6"
}, },
"time": "2024-03-15T18:14:42+00:00" "time": "2024-03-22T18:05:21+00:00"
}, },
{ {
"name": "brick/math", "name": "brick/math",
@ -1408,16 +1408,16 @@
}, },
{ {
"name": "intervention/image", "name": "intervention/image",
"version": "3.4.0", "version": "3.5.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/image.git", "url": "https://github.com/Intervention/image.git",
"reference": "fe1b0e2e64157133322974c28b44c25c2770a0c5" "reference": "408d3655c7705339e8c79731ea7efb51546cfa10"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/fe1b0e2e64157133322974c28b44c25c2770a0c5", "url": "https://api.github.com/repos/Intervention/image/zipball/408d3655c7705339e8c79731ea7efb51546cfa10",
"reference": "fe1b0e2e64157133322974c28b44c25c2770a0c5", "reference": "408d3655c7705339e8c79731ea7efb51546cfa10",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1428,7 +1428,7 @@
"require-dev": { "require-dev": {
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"phpstan/phpstan": "^1", "phpstan/phpstan": "^1",
"phpunit/phpunit": "^9", "phpunit/phpunit": "^10.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8" "squizlabs/php_codesniffer": "^3.8"
}, },
@ -1464,7 +1464,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/image/issues", "issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/3.4.0" "source": "https://github.com/Intervention/image/tree/3.5.0"
}, },
"funding": [ "funding": [
{ {
@ -1476,20 +1476,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-02-14T15:11:21+00:00" "time": "2024-03-13T16:26:15+00:00"
}, },
{ {
"name": "jaybizzle/crawler-detect", "name": "jaybizzle/crawler-detect",
"version": "v1.2.116", "version": "v1.2.117",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git", "url": "https://github.com/JayBizzle/Crawler-Detect.git",
"reference": "97e9fe30219e60092e107651abb379a38b342921" "reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/97e9fe30219e60092e107651abb379a38b342921", "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/6785557f03d0fa9e2205352ebae9a12a4484cc8e",
"reference": "97e9fe30219e60092e107651abb379a38b342921", "reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1526,9 +1526,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/JayBizzle/Crawler-Detect/issues", "issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
"source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.116" "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.117"
}, },
"time": "2023-07-21T15:49:49+00:00" "time": "2024-03-19T22:51:22+00:00"
}, },
{ {
"name": "laminas/laminas-diactoros", "name": "laminas/laminas-diactoros",
@ -1617,16 +1617,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v11.0.7", "version": "v11.0.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "28eabe9dcdcb017a21ce226eda4538c5c8c93b1c" "reference": "0379a7ccb77e2029c43ce508fa76e251a0d68fce"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/28eabe9dcdcb017a21ce226eda4538c5c8c93b1c", "url": "https://api.github.com/repos/laravel/framework/zipball/0379a7ccb77e2029c43ce508fa76e251a0d68fce",
"reference": "28eabe9dcdcb017a21ce226eda4538c5c8c93b1c", "reference": "0379a7ccb77e2029c43ce508fa76e251a0d68fce",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1729,7 +1729,7 @@
"league/flysystem-sftp-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nyholm/psr7": "^1.2", "nyholm/psr7": "^1.2",
"orchestra/testbench-core": "^9.0", "orchestra/testbench-core": "^9.0.6",
"pda/pheanstalk": "^5.0", "pda/pheanstalk": "^5.0",
"phpstan/phpstan": "^1.4.7", "phpstan/phpstan": "^1.4.7",
"phpunit/phpunit": "^10.5|^11.0", "phpunit/phpunit": "^10.5|^11.0",
@ -1818,7 +1818,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2024-03-15T23:17:58+00:00" "time": "2024-03-21T14:15:49+00:00"
}, },
{ {
"name": "laravel/octane", "name": "laravel/octane",
@ -2414,16 +2414,16 @@
}, },
{ {
"name": "league/flysystem", "name": "league/flysystem",
"version": "3.25.0", "version": "3.25.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem.git", "url": "https://github.com/thephpleague/flysystem.git",
"reference": "4c44347133618cccd9b3df1729647a1577b4ad99" "reference": "abbd664eb4381102c559d358420989f835208f18"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4c44347133618cccd9b3df1729647a1577b4ad99", "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/abbd664eb4381102c559d358420989f835208f18",
"reference": "4c44347133618cccd9b3df1729647a1577b4ad99", "reference": "abbd664eb4381102c559d358420989f835208f18",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2488,7 +2488,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/flysystem/issues", "issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.25.0" "source": "https://github.com/thephpleague/flysystem/tree/3.25.1"
}, },
"funding": [ "funding": [
{ {
@ -2500,20 +2500,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-03-09T17:06:45+00:00" "time": "2024-03-16T12:53:19+00:00"
}, },
{ {
"name": "league/flysystem-aws-s3-v3", "name": "league/flysystem-aws-s3-v3",
"version": "3.24.0", "version": "3.25.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513" "reference": "6a5be0e6d6a93574e80805c9cc108a4b63c824d8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/6a5be0e6d6a93574e80805c9cc108a4b63c824d8",
"reference": "809474e37b7fb1d1f8bcc0f8a98bc1cae99aa513", "reference": "6a5be0e6d6a93574e80805c9cc108a4b63c824d8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2553,7 +2553,7 @@
"storage" "storage"
], ],
"support": { "support": {
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.24.0" "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.25.1"
}, },
"funding": [ "funding": [
{ {
@ -2565,20 +2565,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-01-26T18:43:21+00:00" "time": "2024-03-15T19:58:44+00:00"
}, },
{ {
"name": "league/flysystem-local", "name": "league/flysystem-local",
"version": "3.23.1", "version": "3.25.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git", "url": "https://github.com/thephpleague/flysystem-local.git",
"reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00" "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/b884d2bf9b53bb4804a56d2df4902bb51e253f00", "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/61a6a90d6e999e4ddd9ce5adb356de0939060b92",
"reference": "b884d2bf9b53bb4804a56d2df4902bb51e253f00", "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2612,8 +2612,7 @@
"local" "local"
], ],
"support": { "support": {
"issues": "https://github.com/thephpleague/flysystem-local/issues", "source": "https://github.com/thephpleague/flysystem-local/tree/3.25.1"
"source": "https://github.com/thephpleague/flysystem-local/tree/3.23.1"
}, },
"funding": [ "funding": [
{ {
@ -2625,7 +2624,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-01-26T18:25:23+00:00" "time": "2024-03-15T19:58:44+00:00"
}, },
{ {
"name": "league/mime-type-detection", "name": "league/mime-type-detection",
@ -3570,6 +3569,142 @@
], ],
"time": "2024-03-06T16:17:14+00:00" "time": "2024-03-06T16:17:14+00:00"
}, },
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "paragonie/sodium_compat",
"version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
"reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/e592a3e06d1fa0d43988c7c7d9948ca836f644b6",
"reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6",
"shasum": ""
},
"require": {
"paragonie/random_compat": ">=1",
"php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8"
},
"require-dev": {
"phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9"
},
"suggest": {
"ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.",
"ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
},
"type": "library",
"autoload": {
"files": [
"autoload.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com"
},
{
"name": "Frank Denis",
"email": "jedisct1@pureftpd.org"
}
],
"description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
"keywords": [
"Authentication",
"BLAKE2b",
"ChaCha20",
"ChaCha20-Poly1305",
"Chapoly",
"Curve25519",
"Ed25519",
"EdDSA",
"Edwards-curve Digital Signature Algorithm",
"Elliptic Curve Diffie-Hellman",
"Poly1305",
"Pure-PHP cryptography",
"RFC 7748",
"RFC 8032",
"Salpoly",
"Salsa20",
"X25519",
"XChaCha20-Poly1305",
"XSalsa20-Poly1305",
"Xchacha20",
"Xsalsa20",
"aead",
"cryptography",
"ecdh",
"elliptic curve",
"elliptic curve cryptography",
"encryption",
"libsodium",
"php",
"public-key cryptography",
"secret-key cryptography",
"side-channel resistant"
],
"support": {
"issues": "https://github.com/paragonie/sodium_compat/issues",
"source": "https://github.com/paragonie/sodium_compat/tree/v1.20.0"
},
"time": "2023-04-30T00:54:53+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.2", "version": "1.9.2",
@ -4120,16 +4255,16 @@
}, },
{ {
"name": "psy/psysh", "name": "psy/psysh",
"version": "v0.12.1", "version": "v0.12.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/bobthecow/psysh.git", "url": "https://github.com/bobthecow/psysh.git",
"reference": "39621c73e0754328252f032c6997b983afc50431" "reference": "9185c66c2165bbf4d71de78a69dccf4974f9538d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/39621c73e0754328252f032c6997b983afc50431", "url": "https://api.github.com/repos/bobthecow/psysh/zipball/9185c66c2165bbf4d71de78a69dccf4974f9538d",
"reference": "39621c73e0754328252f032c6997b983afc50431", "reference": "9185c66c2165bbf4d71de78a69dccf4974f9538d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4193,9 +4328,70 @@
], ],
"support": { "support": {
"issues": "https://github.com/bobthecow/psysh/issues", "issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.1" "source": "https://github.com/bobthecow/psysh/tree/v0.12.2"
}, },
"time": "2024-03-15T03:22:57+00:00" "time": "2024-03-17T01:53:00+00:00"
},
{
"name": "pusher/pusher-php-server",
"version": "7.2.4",
"source": {
"type": "git",
"url": "https://github.com/pusher/pusher-http-php.git",
"reference": "de2f72296808f9cafa6a4462b15a768ff130cddb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/de2f72296808f9cafa6a4462b15a768ff130cddb",
"reference": "de2f72296808f9cafa6a4462b15a768ff130cddb",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.2",
"paragonie/sodium_compat": "^1.6",
"php": "^7.3|^8.0",
"psr/log": "^1.0|^2.0|^3.0"
},
"require-dev": {
"overtrue/phplint": "^2.3",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Pusher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Library for interacting with the Pusher REST API",
"keywords": [
"events",
"messaging",
"php-pusher-server",
"publish",
"push",
"pusher",
"real time",
"real-time",
"realtime",
"rest",
"trigger"
],
"support": {
"issues": "https://github.com/pusher/pusher-http-php/issues",
"source": "https://github.com/pusher/pusher-http-php/tree/7.2.4"
},
"time": "2023-12-15T10:58:53+00:00"
}, },
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
@ -7438,16 +7634,16 @@
}, },
{ {
"name": "composer/pcre", "name": "composer/pcre",
"version": "3.1.2", "version": "3.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/composer/pcre.git", "url": "https://github.com/composer/pcre.git",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
"reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7489,7 +7685,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/composer/pcre/issues", "issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.1.2" "source": "https://github.com/composer/pcre/tree/3.1.3"
}, },
"funding": [ "funding": [
{ {
@ -7505,7 +7701,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-07T15:38:35+00:00" "time": "2024-03-19T10:26:25+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@ -7807,16 +8003,16 @@
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.29.0", "version": "v1.29.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "e40cc7ffb5186c45698dbd47e9477e0e429396d0" "reference": "8be4a31150eab3b46af11a2e7b2c4632eefaad7e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/e40cc7ffb5186c45698dbd47e9477e0e429396d0", "url": "https://api.github.com/repos/laravel/sail/zipball/8be4a31150eab3b46af11a2e7b2c4632eefaad7e",
"reference": "e40cc7ffb5186c45698dbd47e9477e0e429396d0", "reference": "8be4a31150eab3b46af11a2e7b2c4632eefaad7e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7824,6 +8020,7 @@
"illuminate/contracts": "^9.52.16|^10.0|^11.0", "illuminate/contracts": "^9.52.16|^10.0|^11.0",
"illuminate/support": "^9.52.16|^10.0|^11.0", "illuminate/support": "^9.52.16|^10.0|^11.0",
"php": "^8.0", "php": "^8.0",
"symfony/console": "^6.0|^7.0",
"symfony/yaml": "^6.0|^7.0" "symfony/yaml": "^6.0|^7.0"
}, },
"require-dev": { "require-dev": {
@ -7865,7 +8062,7 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2024-03-08T16:32:33+00:00" "time": "2024-03-20T20:09:31+00:00"
}, },
{ {
"name": "laravel/telescope", "name": "laravel/telescope",
@ -7937,16 +8134,16 @@
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
"version": "1.6.9", "version": "1.6.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/mockery/mockery.git", "url": "https://github.com/mockery/mockery.git",
"reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" "reference": "81a161d0b135df89951abd52296adf97deb0723d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", "url": "https://api.github.com/repos/mockery/mockery/zipball/81a161d0b135df89951abd52296adf97deb0723d",
"reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", "reference": "81a161d0b135df89951abd52296adf97deb0723d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7958,8 +8155,8 @@
"phpunit/phpunit": "<8.0" "phpunit/phpunit": "<8.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5 || ^9.6.10", "phpunit/phpunit": "^8.5 || ^9.6.17",
"symplify/easy-coding-standard": "^12.0.8" "symplify/easy-coding-standard": "^12.1.14"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -8016,7 +8213,7 @@
"security": "https://github.com/mockery/mockery/security/advisories", "security": "https://github.com/mockery/mockery/security/advisories",
"source": "https://github.com/mockery/mockery" "source": "https://github.com/mockery/mockery"
}, },
"time": "2023-12-10T02:24:34+00:00" "time": "2024-03-21T18:34:15+00:00"
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
@ -8405,16 +8602,16 @@
}, },
{ {
"name": "phpstan/phpdoc-parser", "name": "phpstan/phpdoc-parser",
"version": "1.26.0", "version": "1.27.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git", "url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "231e3186624c03d7e7c890ec662b81e6b0405227" "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757",
"reference": "231e3186624c03d7e7c890ec662b81e6b0405227", "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8446,9 +8643,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types", "description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": { "support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues", "issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0"
}, },
"time": "2024-02-23T16:05:55+00:00" "time": "2024-03-21T13:14:53+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -8773,16 +8970,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "10.5.13", "version": "10.5.15",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7" "reference": "86376e05e8745ed81d88232ff92fee868247b07b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/20a63fc1c6db29b15da3bd02d4b6cf59900088a7", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86376e05e8745ed81d88232ff92fee868247b07b",
"reference": "20a63fc1c6db29b15da3bd02d4b6cf59900088a7", "reference": "86376e05e8745ed81d88232ff92fee868247b07b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8854,7 +9051,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.13" "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.15"
}, },
"funding": [ "funding": [
{ {
@ -8870,7 +9067,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-12T15:37:41+00:00" "time": "2024-03-22T04:17:47+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

82
config/broadcasting.php Normal file
View File

@ -0,0 +1,82 @@
<?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

@ -83,6 +83,17 @@ export default defineNuxtConfig({
color: 'gray', color: 'gray',
}, },
}, },
echo: {
broadcaster: process.env.BROADCAST_CONNECTION,
key: process.env.PUSHER_APP_KEY,
cluster: process.env.PUSHER_APP_CLUSTER,
wsHost: process.env.PUSHER_APP_HOST,
wsPort: process.env.PUSHER_APP_PORT,
forceTLS: process.env.PUSHER_APP_TLS,
encrypted: process.env.PUSHER_APP_ENCRYPTED,
disableStats: process.env.PUSHER_APP_DISABLE_STATS,
enabledTransports: ['ws', 'wss'],
},
}, },
}, },
}) })

View File

@ -6,6 +6,8 @@ import NewBookRecommendation from '~/components/modal/NewBookRecommendation.vue'
import CastVote from '~/components/modal/CastVote.vue' import CastVote from '~/components/modal/CastVote.vue'
const dayjs = useDayjs() const dayjs = useDayjs()
const { $echo } = useNuxtApp()
const authStore = useAuthStore()
const bookRecommendationStore = useBookRecommendationStore() const bookRecommendationStore = useBookRecommendationStore()
bookRecommendationStore.fetchRecommendations() bookRecommendationStore.fetchRecommendations()
@ -68,6 +70,20 @@ const sort = ref({
function resolveStatus(status: string) { function resolveStatus(status: string) {
return bookRecommendationStore.statusOptions.find(option => option.value === status) return bookRecommendationStore.statusOptions.find(option => option.value === status)
} }
onMounted(() => {
$echo.private(`BookRecommendation`)
.listen('.BookRecommendationUpdated', (e) => {
bookRecommendationStore.updateRecommendationWS(e.bookRecommendation)
})
.listen('.BookRecommendationDeleted', (e) => {
bookRecommendationStore.deleteRecommendationWS(e.bookRecommendation)
})
.listen('.BookRecommendationCreated', (e) => {
bookRecommendationStore.createRecommendationWS(e.bookRecommendation)
})
authStore.socketId = $echo.socketId()
})
</script> </script>
<template> <template>

View File

@ -4,8 +4,24 @@ import BookInfoCard from '~/components/dashboard/BookInfoCard.vue'
definePageMeta({ middleware: ['auth'] }) definePageMeta({ middleware: ['auth'] })
const bookRecommendationStore = useBookRecommendationStore() const bookRecommendationStore = useBookRecommendationStore()
const authStore = useAuthStore()
const { $echo } = useNuxtApp()
bookRecommendationStore.fetchActiveRecommendations() bookRecommendationStore.fetchActiveRecommendations()
onMounted(() => {
$echo.private(`BookRecommendation`)
.listen('.BookRecommendationUpdated', (e) => {
bookRecommendationStore.updateRecommendationWS(e.bookRecommendation)
})
.listen('.BookRecommendationDeleted', (e) => {
bookRecommendationStore.deleteRecommendationWS(e.bookRecommendation)
})
.listen('.BookRecommendationCreated', (e) => {
bookRecommendationStore.createRecommendationWS(e.bookRecommendation)
})
authStore.socketId = $echo.socketId()
})
</script> </script>
<template> <template>

View File

@ -1,5 +1,5 @@
import { ofetch } from 'ofetch' import { ofetch } from 'ofetch'
import type { FetchOptions } from 'ofetch'; import type { FetchOptions } from 'ofetch'
export default defineNuxtPlugin({ export default defineNuxtPlugin({
name: 'app', name: 'app',
@ -10,10 +10,10 @@ export default defineNuxtPlugin({
const auth = useAuthStore() const auth = useAuthStore()
nuxtApp.provide('storage', (path: string): string => { nuxtApp.provide('storage', (path: string): string => {
if (!path) return '' if (!path) { return '' }
return path.startsWith('http://') || path.startsWith('https://') ? return path.startsWith('http://') || path.startsWith('https://')
path ? path
: config.public.storageBase + path : config.public.storageBase + path
}) })
@ -21,45 +21,54 @@ export default defineNuxtPlugin({
// Initial headers with Accept // Initial headers with Accept
const initialHeaders = { const initialHeaders = {
...headers, ...headers,
'Accept': 'application/json', Accept: 'application/json',
}; }
// Conditionally add server-specific headers // Conditionally add server-specific headers
if (process.server) { if (process.server) {
const serverHeaders = { const serverHeaders = {
'referer': useRequestURL().toString(), referer: useRequestURL().toString(),
...useRequestHeaders(['x-forwarded-for', 'user-agent', 'referer']), ...useRequestHeaders(['x-forwarded-for', 'user-agent', 'referer']),
}; }
Object.assign(initialHeaders, serverHeaders); Object.assign(initialHeaders, serverHeaders)
} }
// Conditionally add authorization header if logged in // Conditionally add authorization header if logged in
if (auth.isLoggedIn) { if (auth.isLoggedIn) {
const authHeaders = { const authHeaders = {
'Authorization': `Bearer ${auth.token}`, Authorization: `Bearer ${auth.token}`,
}; }
Object.assign(initialHeaders, authHeaders); Object.assign(initialHeaders, authHeaders)
} }
return initialHeaders; // Conditionally add X-Socket-ID header if socket is connected
if (auth.isLoggedIn && auth.socketId) {
console.log('auth.socketId', auth.socketId)
const socketHeaders = {
'X-Socket-ID': auth.socketId,
}
Object.assign(initialHeaders, socketHeaders)
}
return initialHeaders
} }
function buildBaseURL(baseURL: string): string { function buildBaseURL(baseURL: string): string {
if (baseURL) return baseURL; if (baseURL) { return baseURL }
return process.server ? return process.server
config.apiLocal + config.public.apiPrefix ? config.apiLocal + config.public.apiPrefix
: config.public.apiBase + config.public.apiPrefix; : config.public.apiBase + config.public.apiPrefix
} }
function buildSecureMethod(options: FetchOptions): void { function buildSecureMethod(options: FetchOptions): void {
if (process.server) return; if (process.server) { return }
const method = options.method?.toLowerCase() ?? 'get' const method = options.method?.toLowerCase() ?? 'get'
if (options.body instanceof FormData && method === 'put') { if (options.body instanceof FormData && method === 'put') {
options.method = 'POST'; options.method = 'POST'
options.body.append('_method', 'PUT'); options.body.append('_method', 'PUT')
} }
} }
@ -67,27 +76,27 @@ export default defineNuxtPlugin({
return !baseURL return !baseURL
&& !path.startsWith('/_nuxt') && !path.startsWith('/_nuxt')
&& !path.startsWith('http://') && !path.startsWith('http://')
&& !path.startsWith('https://'); && !path.startsWith('https://')
} }
globalThis.$fetch = ofetch.create({ globalThis.$fetch = ofetch.create({
retry: false, retry: false,
onRequest({ request, options }) { onRequest({ request, options }) {
if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) return if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) { return }
options.credentials = 'include'; options.credentials = 'include'
options.baseURL = buildBaseURL(options.baseURL ?? ''); options.baseURL = buildBaseURL(options.baseURL ?? '')
options.headers = buildHeaders(options.headers); options.headers = buildHeaders(options.headers)
buildSecureMethod(options); buildSecureMethod(options)
}, },
onRequestError({ error }) { onRequestError({ error }) {
if (process.server) return; if (process.server) { return }
if (error.name === 'AbortError') return; if (error.name === 'AbortError') { return }
useToast().add({ useToast().add({
icon: 'i-heroicons-exclamation-circle-solid', icon: 'i-heroicons-exclamation-circle-solid',
@ -110,7 +119,8 @@ export default defineNuxtPlugin({
color: 'primary', color: 'primary',
}) })
} }
} else if (response.status !== 422) { }
else if (response.status !== 422) {
if (process.client) { if (process.client) {
useToast().add({ useToast().add({
icon: 'i-heroicons-exclamation-circle-solid', icon: 'i-heroicons-exclamation-circle-solid',
@ -119,11 +129,11 @@ export default defineNuxtPlugin({
}) })
} }
} }
} },
} as FetchOptions) } as FetchOptions)
if (auth.isLoggedIn) { if (auth.isLoggedIn) {
await auth.fetchUser(); await auth.fetchUser()
} }
}, },
}) })

View File

@ -0,0 +1,55 @@
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
declare global {
interface Window {
Pusher: typeof Pusher
}
}
window.Pusher = Pusher
export default defineNuxtPlugin(() => {
const runtimeConfig = useRuntimeConfig()
const authStore = useAuthStore()
const echo = new Echo({
broadcaster: runtimeConfig.public.echo.broadcaster,
key: runtimeConfig.public.echo.key,
cluster: 'mt1',
wsHost: runtimeConfig.public.echo.wsHost,
wsPort: runtimeConfig.public.echo.wsPort,
wssPort: runtimeConfig.public.echo.wsPort,
forceTLS: false,
encrypted: runtimeConfig.public.echo.encrypted,
disableStats: runtimeConfig.public.echo.disableStats,
enabledTransports: ['ws', 'wss'],
authorizer: (channel: { name: any }) => {
return {
authorize: async (socketId: string, callback: (error: any, respnse?: any) => void) => {
try {
const response = await $fetch(`/broadcasting/auth`, {
method: 'POST',
body: {
socket_id: socketId,
channel_name: channel.name,
},
})
callback(null, response)
}
catch (error) {
callback(error)
}
},
}
},
})
authStore.socketId = echo.socketId()
// Make Echo instance available through the Nuxt app
return {
provide: {
echo,
},
}
})

View File

@ -24,6 +24,7 @@ export const useAuthStore = defineStore('auth', () => {
maxAge: 60 * 60 * 24 * 365, maxAge: 60 * 60 * 24 * 365,
}) })
const isLoggedIn = computed(() => !!token.value) const isLoggedIn = computed(() => !!token.value)
const socketId = ref('')
const { refresh: logout } = useFetch<any>('logout', { const { refresh: logout } = useFetch<any>('logout', {
method: 'POST', method: 'POST',
@ -49,5 +50,5 @@ export const useAuthStore = defineStore('auth', () => {
}, },
}) })
return { user, isLoggedIn, logout, fetchUser, token } return { user, isLoggedIn, socketId, logout, fetchUser, token }
}) })

View File

@ -21,11 +21,20 @@ export interface BookRecommendation {
email: string email: string
avatar: string avatar: string
} }
votes?: Vote[]
status: BookRecommendationStatusEnum status: BookRecommendationStatusEnum
cover_image?: string cover_image?: string
published_at?: string published_at?: string
} }
export interface Vote {
book_recommendation_id: number
id: number
user_id: number
created_at: string
updated_at: string
}
export const useBookRecommendationStore = defineStore('bookRecommendations', () => { export const useBookRecommendationStore = defineStore('bookRecommendations', () => {
const recommendations = ref<BookRecommendation[]>([]) const recommendations = ref<BookRecommendation[]>([])
@ -72,6 +81,48 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
}, },
}) })
const updateRecommendationWS = async (data: Partial<BookRecommendation>) => {
// This data can be Partial, the id should always be present. We need to only update the properties that are present in the data object.
// We also have a special case for activeRecommendations, in this case we could have a new recommendation that needs to be added to the list. This should only happen if the status is ACTIVE.
// If the Status is not ACTIVE, we need to remove the recommendation from the list.
const index = recommendations.value.findIndex(r => r.id === data.id)
if (index !== -1) {
recommendations.value[index] = { ...recommendations.value[index], ...data }
}
switch (data.status) {
case BookRecommendationStatusEnum.ACTIVE:
const activeIndex = recommendations.value.findIndex(r => r.id === data.id)
if (activeIndex === -1) {
await createRecommendationWS(data)
}
break
default:
const inactiveIndex = recommendations.value.findIndex(r => r.id === data.id)
if (inactiveIndex !== -1) {
recommendations.value.splice(inactiveIndex, 1)
}
break
}
}
const deleteRecommendationWS = async (data: Partial<BookRecommendation>) => {
const index = recommendations.value.findIndex(r => r.id === data.id)
if (index !== -1) {
recommendations.value.splice(index, 1)
}
}
const createRecommendationWS = async (data: Partial<BookRecommendation>) => {
// Here we need to get the missing with data from the server
await useFetch<BookRecommendation>(`book-recommendations/${data.id}?with=recommender,votes`, {
onResponse({ response }) {
if (response.status === 200) {
recommendations.value.push(response._data)
}
},
})
}
function resetRecommendations() { function resetRecommendations() {
recommendations.value = [] recommendations.value = []
} }
@ -79,6 +130,9 @@ export const useBookRecommendationStore = defineStore('bookRecommendations', ()
return { return {
recommendations, recommendations,
resetRecommendations, resetRecommendations,
updateRecommendationWS,
deleteRecommendationWS,
createRecommendationWS,
statusOptions, statusOptions,
fetchRecommendations, fetchRecommendations,
fetchRecommendationsStatus, fetchRecommendationsStatus,

View File

@ -32,5 +32,9 @@
}, },
"resolutions": { "resolutions": {
"vue": "3.4.21" "vue": "3.4.21"
},
"dependencies": {
"laravel-echo": "^1.16.0",
"pusher-js": "8.4.0-rc2"
} }
} }

View File

@ -7,6 +7,14 @@ settings:
overrides: overrides:
vue: 3.4.21 vue: 3.4.21
dependencies:
laravel-echo:
specifier: ^1.16.0
version: 1.16.0
pusher-js:
specifier: 8.4.0-rc2
version: 8.4.0-rc2
devDependencies: devDependencies:
'@antfu/eslint-config': '@antfu/eslint-config':
specifier: ^2.8.3 specifier: ^2.8.3
@ -5910,6 +5918,11 @@ packages:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
dev: true dev: true
/laravel-echo@1.16.0:
resolution: {integrity: sha512-BJGUa4tcKvYmTkzTmcBGMHiO2tq+k7Do5wPmLbRswWfzKwyfZEUR+J5iwBTPEfLLwNPZlA9Kjo6R/NV6pmyIpg==}
engines: {node: '>=10'}
dev: false
/launch-editor@2.6.1: /launch-editor@2.6.1:
resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==} resolution: {integrity: sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==}
dependencies: dependencies:
@ -7762,6 +7775,12 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/pusher-js@8.4.0-rc2:
resolution: {integrity: sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==}
dependencies:
tweetnacl: 1.0.3
dev: false
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
@ -8754,6 +8773,10 @@ packages:
dev: true dev: true
optional: true optional: true
/tweetnacl@1.0.3:
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
dev: false
/type-check@0.4.0: /type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}

14
resources/js/echo.js Normal file
View File

@ -0,0 +1,14 @@
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

@ -27,6 +27,7 @@ Route::prefix('api/v1')->group(function () {
Route::post('devices/disconnect', [AuthController::class, 'deviceDisconnect'])->name('devices.disconnect'); Route::post('devices/disconnect', [AuthController::class, 'deviceDisconnect'])->name('devices.disconnect');
Route::get('devices', [AuthController::class, 'devices'])->name('devices'); Route::get('devices', [AuthController::class, 'devices'])->name('devices');
Route::get('user', [AuthController::class, 'user'])->name('user'); Route::get('user', [AuthController::class, 'user'])->name('user');
Route::match(['get', 'post'], '/broadcasting/auth', [\Illuminate\Broadcasting\BroadcastController::class, 'authenticate'])->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class)->name('broadcasting.auth');
Route::resource('book-recommendations', BookRecommendationController::class); Route::resource('book-recommendations', BookRecommendationController::class);
Route::get('/book-recommendations/{bookRecommendationId}/deadlines', [DeadlineController::class, 'deadlinesByBookRecommendation']); Route::get('/book-recommendations/{bookRecommendationId}/deadlines', [DeadlineController::class, 'deadlinesByBookRecommendation']);

11
routes/channels.php Normal file
View File

@ -0,0 +1,11 @@
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{ulid}', function ($user, $ulid) {
return $user->ulid === $ulid;
});
Broadcast::channel('BookRecommendation', function ($user) {
return $user;
});