chore: Update and add various project dependencies.
This commit is contained in:
parent
52650909a4
commit
af32361e89
36 changed files with 6386 additions and 29 deletions
|
|
@ -14,6 +14,7 @@ ## Foundational Context
|
|||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/horizon (HORIZON) - v5
|
||||
- laravel/nightwatch (NIGHTWATCH) - v1
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/socialite (SOCIALITE) - v5
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ ## Foundational Context
|
|||
- laravel/fortify (FORTIFY) - v1
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/horizon (HORIZON) - v5
|
||||
- laravel/nightwatch (NIGHTWATCH) - v1
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/reverb (REVERB) - v1
|
||||
- laravel/socialite (SOCIALITE) - v5
|
||||
|
|
|
|||
|
|
@ -4,17 +4,22 @@
|
|||
|
||||
use App\Http\Requests\StoreMediaRequest;
|
||||
use App\Models\Media;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Pion\Laravel\ChunkUpload\Handler\ContentRangeUploadHandler;
|
||||
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function store(StoreMediaRequest $request): JsonResponse
|
||||
{
|
||||
$modelClass = $request->input('model');
|
||||
$modelAlias = $request->input('model');
|
||||
$modelId = $request->input('model_id');
|
||||
$collection = $request->input('collection', 'default');
|
||||
|
||||
$modelClass = Relation::getMorphedModel($modelAlias) ?? $modelAlias;
|
||||
$model = $modelClass::findOrFail($modelId);
|
||||
|
||||
$media = $model->addMedia(
|
||||
|
|
@ -30,6 +35,56 @@ public function store(StoreMediaRequest $request): JsonResponse
|
|||
]);
|
||||
}
|
||||
|
||||
public function storeChunked(Request $request): JsonResponse
|
||||
{
|
||||
$receiver = new FileReceiver(
|
||||
UploadedFile::fake()->createWithContent('file', $request->getContent()),
|
||||
$request,
|
||||
ContentRangeUploadHandler::class
|
||||
);
|
||||
|
||||
if (! $receiver->isUploaded()) {
|
||||
return response()->json(['error' => 'File not uploaded'], 400);
|
||||
}
|
||||
|
||||
$save = $receiver->receive();
|
||||
|
||||
if ($save->isFinished()) {
|
||||
$file = $save->getFile();
|
||||
|
||||
$modelAlias = $request->header('X-Model');
|
||||
$modelId = $request->header('X-Model-Id');
|
||||
$collection = $request->header('X-Collection', 'default');
|
||||
|
||||
$modelClass = Relation::getMorphedModel($modelAlias) ?? $modelAlias;
|
||||
$model = $modelClass::findOrFail($modelId);
|
||||
|
||||
$media = $model->addMediaFromPath(
|
||||
$file->getRealPath(),
|
||||
$request->header('X-File-Name', $file->getClientOriginalName()),
|
||||
$collection
|
||||
);
|
||||
|
||||
// Clean up temp file
|
||||
unlink($file->getRealPath());
|
||||
|
||||
return response()->json([
|
||||
'done' => true,
|
||||
'id' => $media->id,
|
||||
'url' => $media->url,
|
||||
'type' => $media->type->value,
|
||||
'original_filename' => $media->original_filename,
|
||||
]);
|
||||
}
|
||||
|
||||
$handler = $save->handler();
|
||||
|
||||
return response()->json([
|
||||
'done' => false,
|
||||
'progress' => $handler->getPercentageDone(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(string $modelId, Media $media): JsonResponse
|
||||
{
|
||||
if ($media->mediable_id !== $modelId) {
|
||||
|
|
@ -48,10 +103,11 @@ public function duplicate(Media $media, Request $request): JsonResponse
|
|||
$duplicates = [];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$modelClass = $target['model'];
|
||||
$modelAlias = $target['model'];
|
||||
$modelId = $target['model_id'];
|
||||
$collection = $target['collection'] ?? $media->collection;
|
||||
|
||||
$modelClass = Relation::getMorphedModel($modelAlias) ?? $modelAlias;
|
||||
$model = $modelClass::findOrFail($modelId);
|
||||
|
||||
$duplicate = $model->media()->create([
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait HasMedia
|
||||
{
|
||||
|
|
@ -70,8 +72,12 @@ public function addMedia(UploadedFile $file, string $collection = 'default', arr
|
|||
|
||||
$mimeType = $file->getMimeType();
|
||||
$type = $this->getMediaType($mimeType);
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
|
||||
$path = $file->store('media/'.now()->format('Y-m'));
|
||||
$filename = Str::uuid().'.'.$extension;
|
||||
$path = 'medias/'.$filename;
|
||||
|
||||
Storage::put($path, file_get_contents($file->getPathname()));
|
||||
|
||||
return $this->media()->create([
|
||||
'collection' => $collection,
|
||||
|
|
@ -85,6 +91,46 @@ public function addMedia(UploadedFile $file, string $collection = 'default', arr
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media from a file path (used for chunked uploads).
|
||||
*/
|
||||
public function addMediaFromPath(string $filePath, string $originalFilename, string $collection = 'default', array $meta = []): Media
|
||||
{
|
||||
if ($this->isSingleMediaCollection($collection)) {
|
||||
$this->clearMediaCollection($collection);
|
||||
}
|
||||
|
||||
$mimeType = mime_content_type($filePath);
|
||||
$type = $this->getMediaType($mimeType);
|
||||
$size = filesize($filePath);
|
||||
$extension = pathinfo($originalFilename, PATHINFO_EXTENSION);
|
||||
|
||||
$filename = Str::uuid().'.'.$extension;
|
||||
$storagePath = 'medias/'.$filename;
|
||||
|
||||
Storage::put($storagePath, file_get_contents($filePath));
|
||||
|
||||
$mediaMeta = [];
|
||||
if ($type === 'image') {
|
||||
$imageInfo = @getimagesize($filePath);
|
||||
if ($imageInfo) {
|
||||
$mediaMeta['width'] = $imageInfo[0];
|
||||
$mediaMeta['height'] = $imageInfo[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->media()->create([
|
||||
'collection' => $collection,
|
||||
'type' => $type,
|
||||
'path' => $storagePath,
|
||||
'original_filename' => $originalFilename,
|
||||
'mime_type' => $mimeType,
|
||||
'size' => $size,
|
||||
'order' => 0,
|
||||
'meta' => array_merge($mediaMeta, $meta),
|
||||
]);
|
||||
}
|
||||
|
||||
public function clearMediaCollection(string $collection = 'default'): void
|
||||
{
|
||||
$this->getMedia($collection)->each(fn (Media $media) => $media->delete());
|
||||
|
|
|
|||
|
|
@ -3,11 +3,26 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Listeners\StripeEventListener;
|
||||
use App\Models\Language;
|
||||
use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\SubscriptionItem;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceHashtag;
|
||||
use App\Models\WorkspaceInvite;
|
||||
use App\Models\WorkspaceLabel;
|
||||
use App\Socialite\InstagramProvider;
|
||||
use App\Socialite\LinkedInPageExtendSocialite;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -15,6 +30,8 @@
|
|||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Cashier\Cashier;
|
||||
use Laravel\Cashier\Events\WebhookReceived;
|
||||
use Laravel\Nightwatch\Facades\Nightwatch;
|
||||
use Laravel\Nightwatch\Records\CacheEvent;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use SocialiteProviders\Facebook\FacebookExtendSocialite;
|
||||
use SocialiteProviders\LinkedIn\LinkedInExtendSocialite;
|
||||
|
|
@ -38,6 +55,7 @@ public function register(): void
|
|||
public function boot(): void
|
||||
{
|
||||
$this->configureDefaults();
|
||||
$this->configureMorphMap();
|
||||
$this->configureSocialite();
|
||||
$this->configureStripeWebhooks();
|
||||
|
||||
|
|
@ -45,6 +63,24 @@ public function boot(): void
|
|||
Cashier::useSubscriptionItemModel(SubscriptionItem::class);
|
||||
}
|
||||
|
||||
protected function configureMorphMap(): void
|
||||
{
|
||||
Relation::enforceMorphMap([
|
||||
'language' => Language::class,
|
||||
'media' => Media::class,
|
||||
'post' => Post::class,
|
||||
'postPlatform' => PostPlatform::class,
|
||||
'socialAccount' => SocialAccount::class,
|
||||
'subscription' => Subscription::class,
|
||||
'subscriptionItem' => SubscriptionItem::class,
|
||||
'user' => User::class,
|
||||
'workspace' => Workspace::class,
|
||||
'workspaceHashtag' => WorkspaceHashtag::class,
|
||||
'workspaceInvite' => WorkspaceInvite::class,
|
||||
'workspaceLabel' => WorkspaceLabel::class,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function configureStripeWebhooks(): void
|
||||
{
|
||||
Event::listen(WebhookReceived::class, StripeEventListener::class);
|
||||
|
|
@ -70,6 +106,9 @@ protected function configureDefaults(): void
|
|||
{
|
||||
Date::use(CarbonImmutable::class);
|
||||
|
||||
// Disable wrapping of JSON resources
|
||||
JsonResource::withoutWrapping();
|
||||
|
||||
DB::prohibitDestructiveCommands(
|
||||
app()->isProduction(),
|
||||
);
|
||||
|
|
@ -83,5 +122,44 @@ protected function configureDefaults(): void
|
|||
->uncompromised()
|
||||
: null
|
||||
);
|
||||
|
||||
Nightwatch::rejectCacheEvents(function (CacheEvent $cacheEvent) {
|
||||
return in_array($cacheEvent->key, [
|
||||
'illuminate:foundation:down',
|
||||
'illuminate:queue:restart',
|
||||
'illuminate:schedule:interrupt',
|
||||
]);
|
||||
});
|
||||
|
||||
// Custom email verification template
|
||||
VerifyEmail::toMailUsing(function (User $user, string $url) {
|
||||
return (new MailMessage)
|
||||
->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject('Confirme o seu endereço de e-mail')
|
||||
->view('mail.email-verification', [
|
||||
'title' => 'Confirme o seu endereço de e-mail',
|
||||
'previewText' => 'Por favor, confirme o seu endereço de e-mail.',
|
||||
'user' => $user,
|
||||
'url' => $url,
|
||||
]);
|
||||
});
|
||||
|
||||
// Custom password reset template
|
||||
ResetPassword::toMailUsing(function (User $user, string $token) {
|
||||
$url = url(route('password.reset', [
|
||||
'token' => $token,
|
||||
'email' => $user->getEmailForPasswordReset(),
|
||||
], false));
|
||||
|
||||
return (new MailMessage)
|
||||
->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->subject('Redefina sua senha')
|
||||
->view('mail.password-reset', [
|
||||
'title' => 'Redefina sua senha',
|
||||
'previewText' => 'Por favor, redefina sua senha.',
|
||||
'user' => $user,
|
||||
'url' => $url,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class FacebookPublisher
|
|||
467, // Invalid access token
|
||||
];
|
||||
|
||||
private string $baseUrl = 'https://graph.facebook.com/v21.0';
|
||||
private string $baseUrl = 'https://graph.facebook.com/v24.0';
|
||||
|
||||
public function publish(PostPlatform $postPlatform): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class InstagramPublisher
|
|||
467, // Invalid access token
|
||||
];
|
||||
|
||||
private string $baseUrl = 'https://graph.facebook.com/v21.0';
|
||||
private string $baseUrl = 'https://graph.instagram.com/v24.0';
|
||||
|
||||
public function publish(PostPlatform $postPlatform): array
|
||||
{
|
||||
|
|
@ -58,7 +58,7 @@ public function publish(PostPlatform $postPlatform): array
|
|||
|
||||
private function publishSingleImage(string $instagramId, string $accessToken, string $content, $media): array
|
||||
{
|
||||
Log::info('Instagram publishing single image', ['instagram_id' => $instagramId]);
|
||||
Log::info('Instagram publishing single image', ['instagram_id' => $instagramId, 'image_url' => $media->url]);
|
||||
|
||||
// Step 1: Create container
|
||||
$containerResponse = Http::post("{$this->baseUrl}/{$instagramId}/media", [
|
||||
|
|
@ -67,6 +67,11 @@ private function publishSingleImage(string $instagramId, string $accessToken, st
|
|||
'access_token' => $accessToken,
|
||||
]);
|
||||
|
||||
Log::info('Instagram container response', [
|
||||
'status' => $containerResponse->status(),
|
||||
'body' => $containerResponse->json(),
|
||||
]);
|
||||
|
||||
if ($containerResponse->failed()) {
|
||||
Log::error('Instagram container creation failed', [
|
||||
'status' => $containerResponse->status(),
|
||||
|
|
@ -75,9 +80,16 @@ private function publishSingleImage(string $instagramId, string $accessToken, st
|
|||
$this->handleApiError($containerResponse, 'Instagram API error');
|
||||
}
|
||||
|
||||
$containerId = $containerResponse->json()['id'];
|
||||
$containerId = $containerResponse->json()['id'] ?? null;
|
||||
|
||||
// Step 2: Publish container
|
||||
if (! $containerId) {
|
||||
throw new \Exception('Instagram container creation failed: No container ID returned');
|
||||
}
|
||||
|
||||
// Step 2: Wait for container to be ready
|
||||
$this->waitForMediaProcessing($containerId, $accessToken);
|
||||
|
||||
// Step 3: Publish container
|
||||
return $this->publishContainer($instagramId, $accessToken, $containerId);
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +113,11 @@ private function publishReel(string $instagramId, string $accessToken, string $c
|
|||
$this->handleApiError($containerResponse, 'Instagram API error');
|
||||
}
|
||||
|
||||
$containerId = $containerResponse->json()['id'];
|
||||
$containerId = $containerResponse->json()['id'] ?? null;
|
||||
|
||||
if (! $containerId) {
|
||||
throw new \Exception('Instagram reel container creation failed: No container ID returned');
|
||||
}
|
||||
|
||||
// Wait for video processing
|
||||
$this->waitForMediaProcessing($containerId, $accessToken);
|
||||
|
|
@ -138,14 +154,16 @@ private function publishStory(string $instagramId, string $accessToken, $media):
|
|||
$this->handleApiError($containerResponse, 'Instagram API error');
|
||||
}
|
||||
|
||||
$containerId = $containerResponse->json()['id'];
|
||||
$containerId = $containerResponse->json()['id'] ?? null;
|
||||
|
||||
// Wait for video processing if needed
|
||||
if ($isVideo) {
|
||||
$this->waitForMediaProcessing($containerId, $accessToken);
|
||||
if (! $containerId) {
|
||||
throw new \Exception('Instagram story container creation failed: No container ID returned');
|
||||
}
|
||||
|
||||
// Step 2: Publish story container
|
||||
// Step 2: Wait for media processing
|
||||
$this->waitForMediaProcessing($containerId, $accessToken);
|
||||
|
||||
// Step 3: Publish story container
|
||||
return $this->publishContainer($instagramId, $accessToken, $containerId);
|
||||
}
|
||||
|
||||
|
|
@ -213,9 +231,16 @@ private function publishCarousel(string $instagramId, string $accessToken, strin
|
|||
$this->handleApiError($carouselResponse, 'Instagram API error');
|
||||
}
|
||||
|
||||
$carouselId = $carouselResponse->json()['id'];
|
||||
$carouselId = $carouselResponse->json()['id'] ?? null;
|
||||
|
||||
// Step 3: Publish carousel
|
||||
if (! $carouselId) {
|
||||
throw new \Exception('Instagram carousel container creation failed: No container ID returned');
|
||||
}
|
||||
|
||||
// Step 3: Wait for carousel to be ready
|
||||
$this->waitForMediaProcessing($carouselId, $accessToken);
|
||||
|
||||
// Step 4: Publish carousel
|
||||
return $this->publishContainer($instagramId, $accessToken, $carouselId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@
|
|||
"laravel/fortify": "^1.30",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/horizon": "^5.42",
|
||||
"laravel/nightwatch": "^1.22",
|
||||
"laravel/reverb": "^1.0",
|
||||
"laravel/socialite": "^5.24",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/wayfinder": "^0.1.9",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"pion/laravel-chunk-upload": "^1.5",
|
||||
"predis/predis": "^3.3",
|
||||
"socialiteproviders/facebook": "^4.1",
|
||||
"socialiteproviders/instagram": "^5.1",
|
||||
|
|
|
|||
162
composer.lock
generated
162
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "162908adce6139201ce70a7e1c4db434",
|
||||
"content-hash": "6afc160a26be910e0994d4a9ceea451c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
|
@ -2070,6 +2070,100 @@
|
|||
},
|
||||
"time": "2026-01-06T14:49:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/nightwatch",
|
||||
"version": "v1.22.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/nightwatch.git",
|
||||
"reference": "a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/nightwatch/zipball/a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110",
|
||||
"reference": "a6ef3f6bccc81e69e17e4f67992c1a3ab6a85110",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"guzzlehttp/promises": "^2.0",
|
||||
"laravel/framework": "^10.0|^11.0|^12.0",
|
||||
"monolog/monolog": "^3.6",
|
||||
"nesbot/carbon": "^2.0|^3.0",
|
||||
"php": "^8.2",
|
||||
"psr/http-message": "^1.0|^2.0",
|
||||
"psr/log": "^1.0|^2.0|^3.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"symfony/console": "^6.0|^7.0",
|
||||
"symfony/http-foundation": "^6.0|^7.0",
|
||||
"symfony/polyfill-php84": "^1.29"
|
||||
},
|
||||
"require-dev": {
|
||||
"aws/aws-sdk-php": "^3.349",
|
||||
"ext-pcntl": "*",
|
||||
"ext-pdo": "*",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"guzzlehttp/psr7": "^2.0",
|
||||
"laravel/horizon": "^5.4",
|
||||
"laravel/pint": "1.21.0",
|
||||
"laravel/vapor-core": "^2.38.2",
|
||||
"livewire/livewire": "^2.0|^3.0",
|
||||
"mockery/mockery": "^1.0",
|
||||
"mongodb/laravel-mongodb": "^4.0|^5.0",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0",
|
||||
"orchestra/testbench-core": "^8.0|^9.0|^10.0",
|
||||
"orchestra/workbench": "^8.0|^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^10.0|^11.0|^12.0",
|
||||
"singlestoredb/singlestoredb-laravel": "^1.0|^2.0",
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"symfony/mailer": "^6.0|^7.0",
|
||||
"symfony/mime": "^6.0|^7.0",
|
||||
"symfony/var-dumper": "^6.0|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Nightwatch": "Laravel\\Nightwatch\\Facades\\Nightwatch"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Nightwatch\\NightwatchServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"agent/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Laravel\\Nightwatch\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "The official Laravel Nightwatch package.",
|
||||
"homepage": "https://nightwatch.laravel.com",
|
||||
"keywords": [
|
||||
"Insights",
|
||||
"laravel",
|
||||
"monitoring"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://nightwatch.laravel.com/docs",
|
||||
"issues": "https://github.com/laravel/nightwatch/issues",
|
||||
"source": "https://github.com/laravel/nightwatch"
|
||||
},
|
||||
"time": "2026-01-15T04:53:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.3.9",
|
||||
|
|
@ -4273,6 +4367,72 @@
|
|||
},
|
||||
"time": "2026-01-12T11:33:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pion/laravel-chunk-upload",
|
||||
"version": "v1.5.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pionl/laravel-chunk-upload.git",
|
||||
"reference": "5cfdb8d9058bb4ecdf3a3100b6c7bb197c21e4d4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pionl/laravel-chunk-upload/zipball/5cfdb8d9058bb4ecdf3a3100b6c7bb197c21e4d4",
|
||||
"reference": "5cfdb8d9058bb4ecdf3a3100b6c7bb197c21e4d4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
|
||||
"illuminate/filesystem": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
|
||||
"illuminate/http": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
|
||||
"illuminate/support": "5.2 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.16.0 | ^3.52.0",
|
||||
"mockery/mockery": "^1.1.0 | ^1.3.0 | ^1.6.0",
|
||||
"overtrue/phplint": "^1.1 | ^2.0 | ^9.1",
|
||||
"phpunit/phpunit": "5.7 | 6.0 | 7.0 | 7.5 | 8.4 | ^8.5 | ^9.3 | ^10.0 | ^11.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Pion\\Laravel\\ChunkUpload\\Providers\\ChunkUploadServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Pion\\Laravel\\ChunkUpload\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Martin Kluska",
|
||||
"email": "martin@kluska.cz"
|
||||
}
|
||||
],
|
||||
"description": "Service for chunked upload with several js providers",
|
||||
"support": {
|
||||
"issues": "https://github.com/pionl/laravel-chunk-upload/issues",
|
||||
"source": "https://github.com/pionl/laravel-chunk-upload/tree/v1.5.6"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://revolut.me/martinpv7n",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/pionl",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-19T16:30:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa",
|
||||
"version": "v9.0.0",
|
||||
|
|
|
|||
45
config/chunk-upload.php
Normal file
45
config/chunk-upload.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @see https://github.com/pionl/laravel-chunk-upload
|
||||
*/
|
||||
|
||||
return [
|
||||
/*
|
||||
* The storage config
|
||||
*/
|
||||
'storage' => [
|
||||
/*
|
||||
* Returns the folder name of the chunks. The location is in storage/app/{folder_name}
|
||||
*/
|
||||
'chunks' => 'chunks',
|
||||
'disk' => 'local',
|
||||
],
|
||||
'clear' => [
|
||||
/*
|
||||
* How old chunks we should delete
|
||||
*/
|
||||
'timestamp' => '-3 HOURS',
|
||||
'schedule' => [
|
||||
'enabled' => true,
|
||||
'cron' => '25 * * * *', // run every hour on the 25th minute
|
||||
],
|
||||
],
|
||||
'chunk' => [
|
||||
// setup for the chunk naming setup to ensure same name upload at same time
|
||||
'name' => [
|
||||
'use' => [
|
||||
'session' => true, // should the chunk name use the session id? The uploader must send cookie!,
|
||||
'browser' => false, // instead of session we can use the ip and browser?
|
||||
],
|
||||
],
|
||||
],
|
||||
'handlers' => [
|
||||
// A list of handlers/providers that will be appended to existing list of handlers
|
||||
'custom' => [],
|
||||
// Overrides the list of handlers - use only what you really want
|
||||
'override' => [
|
||||
// \Pion\Laravel\ChunkUpload\Handler\DropZoneUploadHandler::class
|
||||
],
|
||||
],
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
9
maizzle/.editorconfig
Normal file
9
maizzle/.editorconfig
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
3
maizzle/.gitignore
vendored
Normal file
3
maizzle/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.idea
|
||||
.vscode
|
||||
node_modules
|
||||
48
maizzle/README.md
Normal file
48
maizzle/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<div align="center">
|
||||
<p>
|
||||
<a href="https://maizzle.com" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/maizzle/maizzle/raw/master/.github/logo-dark.svg">
|
||||
<img alt="Maizzle Starter" src="https://github.com/maizzle/maizzle/raw/master/.github/logo-light.svg" width="300" height="225" style="max-width: 100%;">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p>Quickly build HTML emails with Tailwind CSS</p>
|
||||
<div>
|
||||
|
||||
[![Version][npm-version-shield]][npm]
|
||||
[![Build][github-ci-shield]][github-ci]
|
||||
[![Downloads][npm-stats-shield]][npm-stats]
|
||||
[![License][license-shield]][license]
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Run this command and follow the prompts:
|
||||
|
||||
```bash
|
||||
npx create-maizzle
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Maizzle documentation is available at https://maizzle.com
|
||||
|
||||
## Issues
|
||||
|
||||
Please open all issues in the [framework repository](https://github.com/maizzle/framework).
|
||||
|
||||
## License
|
||||
|
||||
The Maizzle framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
[npm]: https://www.npmjs.com/package/@maizzle/framework
|
||||
[npm-stats]: https://npm-stat.com/charts.html?package=%40maizzle%2Fframework&from=2019-03-27
|
||||
[npm-version-shield]: https://img.shields.io/npm/v/@maizzle/framework.svg
|
||||
[npm-stats-shield]: https://img.shields.io/npm/dt/@maizzle/framework.svg?color=6875f5
|
||||
[github-ci]: https://github.com/maizzle/framework/actions
|
||||
[github-ci-shield]: https://github.com/maizzle/framework/actions/workflows/nodejs.yml/badge.svg
|
||||
[license]: ./LICENSE
|
||||
[license-shield]: https://img.shields.io/npm/l/@maizzle/framework.svg?color=0e9f6e
|
||||
53
maizzle/components/button.html
Normal file
53
maizzle/components/button.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script props>
|
||||
// https://maizzle.com/docs/components/button
|
||||
let align = {
|
||||
left: 'text-left',
|
||||
center: 'text-center',
|
||||
right: 'text-right',
|
||||
}[props.align] || ''
|
||||
|
||||
let styles = [
|
||||
'display: inline-block;',
|
||||
'text-decoration: none;',
|
||||
'padding: 16px 24px;',
|
||||
'font-size: 16px;',
|
||||
'line-height: 1;',
|
||||
'border-radius: 8px;',
|
||||
]
|
||||
|
||||
let hasBgClass = () => props.class && props.class.split(' ').some(c => c.startsWith('bg-'))
|
||||
|
||||
if (props['bg-color'] && !hasBgClass()) {
|
||||
styles.push(`background-color: ${props['bg-color']};`)
|
||||
} else {
|
||||
styles.push('background-color: #262626;')
|
||||
}
|
||||
|
||||
if (props.color) {
|
||||
styles.push(`color: ${props.color};`)
|
||||
} else {
|
||||
styles.push('color: #ffffff;')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
align,
|
||||
href: props.href,
|
||||
styles: styles.join(''),
|
||||
msoPt: props['mso-pt'] || '16px',
|
||||
msoPb: props['mso-pb'] || '31px',
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="{{ align }}">
|
||||
<a attributes href="{{ href }}" style="{{ styles }}">
|
||||
<outlook trim>
|
||||
<i class="mso-font-width-[150%]" style="mso-text-raise: {{ msoPb }};" hidden> </i>
|
||||
</outlook>
|
||||
<span style="mso-text-raise: {{ msoPt }}" trim>
|
||||
<yield />
|
||||
</span>
|
||||
<outlook trim>
|
||||
<i class="mso-font-width-[150%]" hidden> ​</i>
|
||||
</outlook>
|
||||
</a>
|
||||
</div>
|
||||
70
maizzle/components/divider.html
Normal file
70
maizzle/components/divider.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script props>
|
||||
// https://maizzle.com/docs/components/divider
|
||||
let styles = [
|
||||
`height: ${props.height || '1px'};`,
|
||||
`line-height: ${props.height || '1px'};`,
|
||||
]
|
||||
|
||||
/**
|
||||
* Color
|
||||
*
|
||||
* If a Tailwind background color class was passed, use it.
|
||||
* Otherwise, the `color` prop will take precedence if
|
||||
* as long as it was passed.
|
||||
*/
|
||||
let hasBgClass = () => props.class && props.class.split(' ').some(c => c.startsWith('bg-'))
|
||||
|
||||
if (props.color) {
|
||||
styles.push(`background-color: ${props.color};`)
|
||||
}
|
||||
if (!props.color && !hasBgClass()) {
|
||||
styles.push(`background-color: #cbd5e1;`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Margins
|
||||
*
|
||||
* If any margin prop was passed, add `margin: 0` first.
|
||||
* It's important that this comes first, so inlining
|
||||
* does not use it to override existing margins.
|
||||
*/
|
||||
if (props.top || props.bottom || props.left || props.right || props['space-y'] || props['space-x']) {
|
||||
styles.push('margin: 0;')
|
||||
}
|
||||
|
||||
props['space-y'] = props['space-y'] === 0 ? '0px' : props['space-y'] || '24px'
|
||||
if (props['space-y']) {
|
||||
styles.push(`margin-top: ${props['space-y']}; margin-bottom: ${props['space-y']};`)
|
||||
}
|
||||
|
||||
props['space-x'] = props['space-x'] === 0 ? '0px' : props['space-x']
|
||||
if (props['space-x']) {
|
||||
styles.push(`margin-left: ${props['space-x']}; margin-right: ${props['space-x']};`)
|
||||
}
|
||||
|
||||
props.top = props.top === 0 ? '0px' : props.top
|
||||
if (props.top) {
|
||||
styles.push(`margin-top: ${props.top};`)
|
||||
}
|
||||
|
||||
props.bottom = props.bottom === 0 ? '0px' : props.bottom
|
||||
if (props.bottom) {
|
||||
styles.push(`margin-bottom: ${props.bottom};`)
|
||||
}
|
||||
|
||||
props.left = props.left === 0 ? '0px' : props.left
|
||||
if (props.left) {
|
||||
styles.push(`margin-left: ${props.left};`)
|
||||
}
|
||||
|
||||
props.right = props.right === 0 ? '0px' : props.right
|
||||
if (props.right) {
|
||||
styles.push(`margin-right: ${props.right};`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
styles: styles.join(''),
|
||||
}
|
||||
</script>
|
||||
|
||||
<div role="separator" style="{{ styles }}">‍</div>
|
||||
44
maizzle/components/footer.html
Normal file
44
maizzle/components/footer.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script props>
|
||||
module.exports = {
|
||||
unsubscribe_url: props.unsubscribe_url
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="text-center text-zinc-600 text-xs p-6">
|
||||
<p class="m-0 mb-2">
|
||||
Enviado por <a href="https://clinyx.com.br" class="text-zinc-600">Clinyx</a> - Sistema para gestão de clínicas e consultórios
|
||||
</p>
|
||||
|
||||
<p class="m-0 cursor-default">
|
||||
<a href="https://www.linkedin.com/company/clinyx" target="_blank"
|
||||
class="text-zinc-600 [text-decoration:none] hover:![text-decoration:underline]">
|
||||
LinkedIn
|
||||
</a>
|
||||
•
|
||||
<a href="https://www.facebook.com/clinyx" target="_blank"
|
||||
class="text-zinc-600 [text-decoration:none] hover:![text-decoration:underline]">
|
||||
Facebook
|
||||
</a>
|
||||
•
|
||||
<a href="https://instagram.com/useclinyx" target="_blank"
|
||||
class="text-zinc-600 [text-decoration:none] hover:![text-decoration:underline]">
|
||||
Instagram
|
||||
</a>
|
||||
•
|
||||
<a href="https://youtube.com/@useclinyx" target="_blank"
|
||||
class="text-zinc-600 [text-decoration:none] hover:![text-decoration:underline]">
|
||||
YouTube
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@if(isset($unsubscribe_url))
|
||||
<p class="m-0 mt-2">
|
||||
<a href="{{ unsubscribe_url }}" target="_blank"
|
||||
class="text-zinc-600 [text-decoration:none] hover:![text-decoration:underline]">
|
||||
Unsubscribe
|
||||
</a>
|
||||
</p>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
5
maizzle/components/header.html
Normal file
5
maizzle/components/header.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<div class="my-12 sm:my-8 text-center">
|
||||
<a href="https://trypost.it" target="_blank">
|
||||
<img src="@{{ asset('/images/emails/trypost/logo-header.png') }}" width="160" alt="Trypost">
|
||||
</a>
|
||||
</div>
|
||||
24
maizzle/components/spacer.html
Normal file
24
maizzle/components/spacer.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script props>
|
||||
// https://maizzle.com/docs/components/spacer
|
||||
let styles = []
|
||||
|
||||
if (props.height) {
|
||||
styles.push(`line-height: ${props.height};`)
|
||||
}
|
||||
|
||||
if (props['mso-height']) {
|
||||
styles.push(`mso-line-height-alt: ${props['mso-height']};`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
height: props.height,
|
||||
styles: styles.join('')
|
||||
}
|
||||
</script>
|
||||
|
||||
<if condition="height">
|
||||
<div role="separator" style="{{ styles }}">‍</div>
|
||||
</if>
|
||||
<else>
|
||||
<div role="separator">‍</div>
|
||||
</else>
|
||||
25
maizzle/components/v-fill.html
Normal file
25
maizzle/components/v-fill.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script props>
|
||||
// https://maizzle.com/docs/components/vml#v-fill
|
||||
module.exports = {
|
||||
width: props.width || '600px',
|
||||
type: props.type || 'frame',
|
||||
sizes: props.sizes,
|
||||
origin: props.origin,
|
||||
position: props.position,
|
||||
aspect: props.aspect,
|
||||
color: props.color,
|
||||
inset: props.inset || '0,0,0,0',
|
||||
stroke: props.stroke || 'f',
|
||||
strokecolor: props.strokecolor,
|
||||
fill: props.fill || 't',
|
||||
fillcolor: props.fillcolor || 'none',
|
||||
image: props.image || 'https://via.placeholder.com/600x400'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--[if mso]>
|
||||
<v:rect fill="{{ fillcolor ? 't' : fill }}" stroke="{{ strokecolor ? 't' : stroke }}" style="width: {{ width }}" xmlns:v="urn:schemas-microsoft-com:vml"{{{ strokecolor ? ` strokecolor="${strokecolor}"` : '' }}}{{{ fillcolor ? ` fillcolor="${fillcolor}"` : '' }}}>
|
||||
<v:fill type="{{ type }}" src="{{{ image }}}"{{{ sizes ? ` sizes="${sizes}"` : '' }}}{{{ aspect ? ` aspect="${aspect}"` : '' }}}{{{ origin ? ` origin="${origin}"` : '' }}}{{{ position ? ` position="${position}"` : '' }}}{{{ color ? ` color="${color}"` : '' }}} />
|
||||
<v:textbox inset="{{ inset }}" style="mso-fit-shape-to-text: true"><div><![endif]-->
|
||||
<yield />
|
||||
<!--[if mso]></div></v:textbox></v:rect><![endif]-->
|
||||
14
maizzle/components/v-image.html
Normal file
14
maizzle/components/v-image.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script props>
|
||||
module.exports = {
|
||||
width: props.width || '600px',
|
||||
height: props.height || '400px',
|
||||
image: props.image || 'https://placehold.co/600x400'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--[if mso]>
|
||||
<v:image src="{!! image !!}" style="width: {{ width }}; height: {{ height }};" xmlns:v="urn:schemas-microsoft-com:vml" />
|
||||
<v:rect fill="f" stroke="f" style="position: absolute; width: {{ width }}; height: {{ height }};" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
<v:textbox inset="0,0,0,0"><div><![endif]-->
|
||||
<yield />
|
||||
<!--[if mso]></div></v:textbox></v:rect><![endif]-->
|
||||
18
maizzle/config.js
Normal file
18
maizzle/config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/** @type {import('@maizzle/framework').Config} */
|
||||
export default {
|
||||
build: {
|
||||
content: ['templates/**/*.html'],
|
||||
static: {
|
||||
source: ['images/**/*.*'],
|
||||
destination: 'images',
|
||||
},
|
||||
output: {
|
||||
path: 'build_local',
|
||||
},
|
||||
},
|
||||
posthtml: {
|
||||
expressions: {
|
||||
unescapeDelimiters: ['{!!', '!!}'],
|
||||
},
|
||||
},
|
||||
};
|
||||
26
maizzle/config.production.js
Normal file
26
maizzle/config.production.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/** @type {import('@maizzle/framework').Config} */
|
||||
export default {
|
||||
build: {
|
||||
content: ['templates/**/*.html'],
|
||||
static: {
|
||||
source: ['images/**/*.*'],
|
||||
destination: '../../../public/images/emails',
|
||||
},
|
||||
output: {
|
||||
path: '../resources/views/mail',
|
||||
extension: 'blade.php',
|
||||
from: 'templates',
|
||||
},
|
||||
},
|
||||
posthtml: {
|
||||
expressions: {
|
||||
unescapeDelimiters: ['{!!', '!!}'],
|
||||
},
|
||||
},
|
||||
css: {
|
||||
inline: true,
|
||||
purge: true,
|
||||
shorthand: true,
|
||||
},
|
||||
prettify: true,
|
||||
};
|
||||
BIN
maizzle/images/logo.png
Normal file
BIN
maizzle/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6 KiB |
53
maizzle/layouts/main.html
Normal file
53
maizzle/layouts/main.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="pt-BR" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="supported-color-schemes" content="light">
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
td,th,div,p,a,h1,h2,h3,h4,h5,h6 {font-family: "Segoe UI", sans-serif; mso-line-height-rule: exactly;}
|
||||
.mso-break-all {word-break: break-all;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
@if(isset($title))
|
||||
<title>@{{ $title }}</title>
|
||||
@endif
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" media="screen">
|
||||
<style>
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
img {
|
||||
@apply max-w-full align-middle;
|
||||
}
|
||||
</style>
|
||||
<stack name="head" />
|
||||
</head>
|
||||
|
||||
<body class="m-0 p-0 w-full [word-break:break-word] [-webkit-font-smoothing:antialiased] {{ page.bodyClass || '' }}">
|
||||
@if(isset($preheader))
|
||||
<div class="hidden">
|
||||
@{{ $preheader }}
|
||||
<each loop="item in Array.from(Array(150))"> ͏ </each>
|
||||
</div>
|
||||
@endif
|
||||
<div role="article" aria-roledescription="email" aria-label="@{{ $title }}" lang="pt-BR">
|
||||
<yield />
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
5262
maizzle/package-lock.json
generated
Normal file
5262
maizzle/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
maizzle/package.json
Normal file
12
maizzle/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "maizzle serve",
|
||||
"build": "maizzle build production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@maizzle/framework": "latest",
|
||||
"tailwindcss-preset-email": "latest"
|
||||
}
|
||||
}
|
||||
11
maizzle/tailwind.config.js
Normal file
11
maizzle/tailwind.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
presets: [
|
||||
require('tailwindcss-preset-email'),
|
||||
],
|
||||
content: [
|
||||
'./components/**/*.html',
|
||||
'./templates/**/*.html',
|
||||
'./layouts/**/*.html',
|
||||
],
|
||||
}
|
||||
44
maizzle/templates/email-verification.html
Normal file
44
maizzle/templates/email-verification.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<x-main>
|
||||
<div class="bg-zinc-50 sm:px-4 font-sans">
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td class="w-[552px] max-w-full">
|
||||
<x-header />
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td class="p-12 sm:px-6 text-base text-zinc-800 bg-white rounded shadow-sm">
|
||||
<p class="leading-6 mt-0">
|
||||
Olá @{{$user->name}},
|
||||
</p>
|
||||
|
||||
<p class="leading-6">
|
||||
Por favor, confirme seu endereço de e-mail clicando no botão abaixo:
|
||||
</p>
|
||||
|
||||
<x-spacer height="24px" />
|
||||
|
||||
<div class="text-center">
|
||||
<x-button href="@{{ $url }}">
|
||||
Confirmar Endereço de E-mail →
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<x-spacer height="24px" />
|
||||
|
||||
<p class="leading-6">
|
||||
Se você não criou esta conta, pode ignorar este e-mail com segurança.
|
||||
</p>
|
||||
<p class="leading-6 mb-0">
|
||||
Atenciosamente,
|
||||
<br />
|
||||
Equipe Clinyx
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<x-footer />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</x-main>
|
||||
41
maizzle/templates/invite.html
Normal file
41
maizzle/templates/invite.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
bodyClass: bg-slate-50
|
||||
preheader: Convite para o sistema da Clinyx
|
||||
---
|
||||
<x-main>
|
||||
<div class="bg-zinc-50 sm:px-4 font-sans">
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td class="w-[552px] max-w-full">
|
||||
<x-header />
|
||||
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td class="p-12 sm:px-6 text-base text-zinc-700 bg-white rounded shadow-sm">
|
||||
<h1 class="m-0 mb-6 text-2xl sm:leading-8 text-black font-semibold">
|
||||
Olá 👋
|
||||
</h1>
|
||||
|
||||
<p class="m-0 leading-6">
|
||||
Você foi convidado para acessar o sistema da Clinyx.
|
||||
<br />
|
||||
<br />
|
||||
Para aceitar o convite e começar a utilizar a plataforma, clique no botão abaixo.
|
||||
</p>
|
||||
|
||||
<x-spacer height="24px" />
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<x-button href="@{{ $url }}">
|
||||
Aceitar convite →
|
||||
</x-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<x-footer />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</x-main>
|
||||
44
maizzle/templates/password-reset.html
Normal file
44
maizzle/templates/password-reset.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<x-main>
|
||||
<div class="bg-zinc-50 sm:px-4 font-sans">
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td class="w-[552px] max-w-full">
|
||||
<x-header />
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td class="p-12 sm:px-6 text-base text-zinc-800 bg-white rounded shadow-sm">
|
||||
<p class="leading-6 mt-0">
|
||||
Olá @{{$user->name}},
|
||||
</p>
|
||||
|
||||
<p class="leading-6">
|
||||
Recebemos uma solicitação para redefinir a senha da sua conta. Clique no botão abaixo para criar uma nova senha:
|
||||
</p>
|
||||
|
||||
<x-spacer height="24px" />
|
||||
|
||||
<div class="text-center">
|
||||
<x-button href="@{{ $url }}">
|
||||
Redefinir Senha →
|
||||
</x-button>
|
||||
</div>
|
||||
|
||||
<x-spacer height="24px" />
|
||||
|
||||
<p class="leading-6">
|
||||
Este link expira em 60 minutos. Se você não solicitou a redefinição de senha, pode ignorar este e-mail com segurança.
|
||||
</p>
|
||||
<p class="leading-6 mb-0">
|
||||
Atenciosamente,
|
||||
<br />
|
||||
Equipe Clinyx
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<x-footer />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</x-main>
|
||||
BIN
public/images/emails/logo-header.png
Normal file
BIN
public/images/emails/logo-header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -11,6 +11,13 @@ import {
|
|||
IconBookmark,
|
||||
IconMusic,
|
||||
} from '@tabler/icons-vue';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
|
||||
interface SocialAccount {
|
||||
id: string;
|
||||
|
|
@ -27,11 +34,18 @@ interface MediaItem {
|
|||
original_filename: string;
|
||||
}
|
||||
|
||||
interface ContentTypeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
socialAccount: SocialAccount;
|
||||
content: string;
|
||||
media: MediaItem[];
|
||||
contentType?: string;
|
||||
contentTypeOptions?: ContentTypeOption[];
|
||||
charCount: number;
|
||||
maxLength: number;
|
||||
isValid: boolean;
|
||||
|
|
@ -45,17 +59,42 @@ const props = defineProps<Props>();
|
|||
const isReel = computed(() => props.contentType === 'instagram_reel');
|
||||
const isStory = computed(() => props.contentType === 'instagram_story');
|
||||
const isFeed = computed(() => props.contentType === 'instagram_feed' || !props.contentType);
|
||||
const hasMultipleContentTypes = computed(() => (props.contentTypeOptions?.length || 0) > 1);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:content': [value: string];
|
||||
'update:contentType': [value: string];
|
||||
'upload': [event: Event];
|
||||
'remove-media': [mediaId: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Reel/Story Preview (vertical) -->
|
||||
<div v-if="isReel || isStory" class="mx-auto" style="max-width: 320px;">
|
||||
<div class="space-y-4">
|
||||
<!-- Content Type Selector -->
|
||||
<div v-if="hasMultipleContentTypes" class="flex items-center justify-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">Type:</span>
|
||||
<Select
|
||||
:model-value="contentType"
|
||||
@update:model-value="emit('update:contentType', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-[140px] h-8">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in contentTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Reel/Story Preview (vertical) -->
|
||||
<div v-if="isReel || isStory" class="mx-auto" style="max-width: 320px;">
|
||||
<div class="relative bg-black rounded-2xl overflow-hidden" style="aspect-ratio: 9/16;">
|
||||
<!-- Media Area -->
|
||||
<div v-if="media.length > 0" class="w-full h-full">
|
||||
|
|
@ -367,4 +406,5 @@ const emit = defineEmits<{
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ import DatePicker from '@/components/DatePicker.vue';
|
|||
import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue';
|
||||
import { calendar } from '@/routes';
|
||||
import { destroy as destroyPost, update as updatePost } from '@/routes/posts';
|
||||
import { store as storeMedia, destroy as destroyMedia, duplicate as duplicateMedia } from '@/routes/medias';
|
||||
import { store as storeMedia, storeChunked as storeMediaChunked, destroy as destroyMedia, duplicate as duplicateMedia } from '@/actions/App/Http/Controllers/MediaController';
|
||||
import { uploadChunked, shouldUseChunkedUpload } from '@/utils/chunkedUpload';
|
||||
import { type BreadcrumbItemType } from '@/types';
|
||||
|
||||
interface SocialAccount {
|
||||
|
|
@ -517,14 +518,30 @@ const handleFileUpload = async (event: Event, postPlatformId: string) => {
|
|||
|
||||
for (const file of Array.from(files)) {
|
||||
try {
|
||||
// 1. Upload once to the current platform
|
||||
const formData = new FormData();
|
||||
formData.append('media', file);
|
||||
formData.append('model', 'App\\Models\\PostPlatform');
|
||||
formData.append('model_id', postPlatformId);
|
||||
let data;
|
||||
|
||||
const response = await axios.post(storeMedia.url(), formData);
|
||||
const data = response.data;
|
||||
// Use chunked upload for large files (> 10MB)
|
||||
if (shouldUseChunkedUpload(file)) {
|
||||
data = await uploadChunked({
|
||||
file,
|
||||
url: storeMediaChunked.url(),
|
||||
model: 'postPlatform',
|
||||
modelId: postPlatformId,
|
||||
collection: 'default',
|
||||
onProgress: (progress) => {
|
||||
console.log(`Upload progress: ${progress}%`);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Regular upload for small files
|
||||
const formData = new FormData();
|
||||
formData.append('media', file);
|
||||
formData.append('model', 'postPlatform');
|
||||
formData.append('model_id', postPlatformId);
|
||||
|
||||
const response = await axios.post(storeMedia.url(), formData);
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
// Add to current platform (use spread for reactivity)
|
||||
const currentMedia = platformMedia.value[postPlatformId] || [];
|
||||
|
|
@ -533,7 +550,7 @@ const handleFileUpload = async (event: Event, postPlatformId: string) => {
|
|||
// 2. If synced, duplicate to other platforms
|
||||
if (otherPlatformIds.length > 0) {
|
||||
const targets = otherPlatformIds.map(id => ({
|
||||
model: 'App\\Models\\PostPlatform',
|
||||
model: 'postPlatform',
|
||||
model_id: id,
|
||||
}));
|
||||
|
||||
|
|
|
|||
80
resources/js/utils/chunkedUpload.ts
Normal file
80
resources/js/utils/chunkedUpload.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import axios from 'axios';
|
||||
|
||||
interface ChunkedUploadOptions {
|
||||
file: File;
|
||||
url: string;
|
||||
model: string;
|
||||
modelId: string;
|
||||
collection?: string;
|
||||
chunkSize?: number;
|
||||
onProgress?: (progress: number) => void;
|
||||
onComplete?: (response: any) => void;
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
interface ChunkedUploadResult {
|
||||
id: string;
|
||||
url: string;
|
||||
type: string;
|
||||
original_filename: string;
|
||||
}
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
|
||||
|
||||
export async function uploadChunked(options: ChunkedUploadOptions): Promise<ChunkedUploadResult> {
|
||||
const {
|
||||
file,
|
||||
url,
|
||||
model,
|
||||
modelId,
|
||||
collection = 'default',
|
||||
chunkSize = DEFAULT_CHUNK_SIZE,
|
||||
onProgress,
|
||||
onComplete,
|
||||
onError,
|
||||
} = options;
|
||||
|
||||
const totalSize = file.size;
|
||||
const totalChunks = Math.ceil(totalSize / chunkSize);
|
||||
let uploadedBytes = 0;
|
||||
|
||||
try {
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
const start = chunkIndex * chunkSize;
|
||||
const end = Math.min(start + chunkSize, totalSize);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const response = await axios.post(url, chunk, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Range': `bytes ${start}-${end - 1}/${totalSize}`,
|
||||
'X-Model': model,
|
||||
'X-Model-Id': modelId,
|
||||
'X-Collection': collection,
|
||||
'X-File-Name': file.name,
|
||||
},
|
||||
});
|
||||
|
||||
uploadedBytes = end;
|
||||
const progress = Math.round((uploadedBytes / totalSize) * 100);
|
||||
onProgress?.(progress);
|
||||
|
||||
if (response.data.done) {
|
||||
onComplete?.(response.data);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Upload did not complete');
|
||||
} catch (error) {
|
||||
onError?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold for when to use chunked upload (10MB)
|
||||
const CHUNKED_UPLOAD_THRESHOLD = 10 * 1024 * 1024;
|
||||
|
||||
export function shouldUseChunkedUpload(file: File): boolean {
|
||||
return file.size > CHUNKED_UPLOAD_THRESHOLD;
|
||||
}
|
||||
|
|
@ -131,6 +131,7 @@
|
|||
|
||||
// Media
|
||||
Route::post('medias', [MediaController::class, 'store'])->name('medias.store');
|
||||
Route::post('medias/chunked', [MediaController::class, 'storeChunked'])->name('medias.store-chunked');
|
||||
Route::post('medias/{media}/duplicate', [MediaController::class, 'duplicate'])->name('medias.duplicate');
|
||||
Route::delete('medias/{modelId}/{media}', [MediaController::class, 'destroy'])->name('medias.destroy');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue