Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
# change accordingly if you are not using Laravel Sail
FORWARD_MAILPIT_PORT=2525
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT="${FORWARD_MAILPIT_PORT:-2525}"
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
name: Build and deploy Application

# on:
# push:
# branches:
# - prod
# workflow_dispatch:
on:
push:
branches:
- prod
workflow_dispatch:
workflow_run:
workflows: [Tests]
types: [completed]

jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'prod' }} # Run only if the first workflow succeeded and push was to prod
steps:
- uses: actions/checkout@v4
- name: Setup PHP
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Tests

on: [push]
jobs:
tests:
runs-on: ubuntu-latest
env:
APP_URL: "http://127.0.0.1:8000"
DB_USERNAME: root
DB_DATABASE: tasktango
DB_HOST: 127.0.0.1
DB_PASSWORD: root
MAIL_MAILER: log
SUPER_ADMIN_USERNAME: super_admin
SUPER_ADMIN_EMAIL: admin@admin.com
SUPER_ADMIN_PASSWORD: super-secure-password
steps:
- uses: actions/checkout@v4
- name: Prepare The Environment
run: cp .env.example .env
- name: Create Database
run: |
sudo systemctl start mysql
mysql --user="root" --password="root" -e "CREATE DATABASE \`tasktango\` character set UTF8mb4 collate utf8mb4_bin;"
- name: Install Composer Dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Generate Application Key
run: php artisan key:generate
- name: Run Laravel Server
run: php artisan serve --no-reload &
- name: Run Tests
run: php artisan test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ justfile
.env.testing
.env.production
.phpunit.result.cache
.phpunit*

/public/hot
/public/storage
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.0
hooks:
- id: gitleaks
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,3 @@ Inbox View

![Search](.screenshots/search-functionality.png)
Search Functionality

2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<directory>tests/Feature</directory>
</testsuite>
<testsuite name="Main">
<directory>tests/Feature/Main</directory>
<directory>tests/Main</directory>
</testsuite>
</testsuites>
<source>
Expand Down
8 changes: 6 additions & 2 deletions resources/views/components/avatar-or-icon.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
$showAvatar = false;
@endphp
@if ($showAvatar && !$forceToShowIcon)
<x-avatar :image="$avatar" class="{{$attributes->get('avatar-class')}}" />
<x-avatar :image="$avatar"
{{ $attributes->except(['class', 'icon-class', 'avatar-class']) }}
class="{{$attributes->get('avatar-class')}}" />
@else
<x-icon name="o-user" class="{{$attributes->get('icon-class')}}" />
<x-icon name="o-user"
{{ $attributes->except(['class', 'icon-class', 'avatar-class','name']) }}
class="{{$attributes->get('icon-class')}}" />
@endif
2 changes: 1 addition & 1 deletion resources/views/livewire/layout/sidebar.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<x-list-item :item="$user" value="full_name" sub-value="email" no-separator no-hover
class="-mx-2 !-my-2 rounded">
<x-slot:avatar>
<x-avatar-or-icon :user="$user" avatar-class="!w-10"/>
<x-avatar-or-icon dusk="profile-avatar" :user="$user" avatar-class="!w-10"/>
</x-slot:avatar>
<x-slot:actions>
<x-theme-toggle dusk="theme-controller" darkTheme="synthwave" class="!w-6" />
Expand Down
1 change: 0 additions & 1 deletion resources/views/livewire/segments/search.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,3 @@ public function updatedNoDueDate($value): void
{{-- task modal ready --}}
<livewire:task.modal />
</div>

85 changes: 70 additions & 15 deletions resources/views/livewire/set-profile-picture.blade.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<?php

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use Mary\Traits\Toast;

new
#[Layout('layouts.guest', ['hideBackButton' => true])]
class extends Component
{
use WithFileUploads;
use Toast;

public $avatar;

function mount()
{
if (!auth()->user()->profile_picture && !auth()->user()->has_asked_for_profile_picture) {
Expand All @@ -20,18 +25,56 @@ function mount()
return redirect()->route('index');
}

function continue()
function setPicture()
{
if (auth()->user()->profile_picture) {
auth()->user()->has_asked_for_profile_picture = true;
return redirect()->route('index');
}
$this->warning(
title: 'Profile Picture Required',
position: 'toast-bottom toast-end text-wrap',
description: "Please select your profile picture before proceeding, if you don't want to click Skip for now. If you have changed your profile picture right now but can't move on wait a few seconds and click Next again",
icon: 'o-exclamation-triangle'
);
try {
$validated = $this->validate(
['avatar' => 'image|max:2048'],
[
'avatar.image' => 'The avatar must be an image.',
'avatar.max' => 'Your profile picture may not be greater than 1MB.',
]
);

// Validation passed, proceed with logic
} catch (ValidationException $e) {
$errors = $e->validator->errors(); // Get all validation errors

// Example: Get all error messages as an array
$errorMessages = $errors->all();

// Example: Get errors for a specific field
$avatarErrors = $errors->get('avatar');

// Handle errors (store them in session, return JSON, etc.)
$this->warning(
title: $avatarErrors[0],
position: 'toast-bottom toast-end text-wrap',
icon: 'o-exclamation-triangle'
);
return;
}

$user = auth()->user();
$diskToStore = config('filesystems.default') === 'local' ? 'public' : config('filesystems.default');
$path = $this->avatar->store('profile_pictures', $diskToStore);

if ($user->profile_picture) {
Storage::delete($user->profile_picture);
}
if ($diskToStore === 'public') {
$user->profile_picture = $path;
} else {
// making the temporaryUrl not that temporary
$user->profile_picture = Storage::disk($diskToStore)->temporaryUrl($path, now()->addYears(100));
}

$user->save();
return redirect()->route('index');
}

function skipForNow()
Expand All @@ -46,13 +89,25 @@ function skipForNow()
<x-card class="bg-base-200" title="Set your profile picture" subtitle="Complete your profile with a personal touch">
<div class="flex flex-col">
<div class="flex items-center justify-center w-full flex-1">
<livewire:profile.update-profile-picture />

<x-form class="flex flex-col items-center" wire:submit="updateProfilePicture">
@csrf
<x-file wire:model="avatar" accept="image/png, image/jpeg" name="profile-picture"
class="relative text-wrap mx-auto flex flex-col items-center">
<div class="avatar">
<div class="w-32 relative rounded-full overflow-hidden" >
<x-icon name="c-user-circle" class="text-secondary" class="!w-32 text-secondary" x-show="!$wire.avatar"/>
<img src="">
</div>
</div>
<div class="absolute bottom-0 right-0 bg-primary text-white rounded-full p-2 w-8 h-8 flex items-center">
<x-icon name="o-camera" class="!w-8 rounded-full" />
</div>
</x-file>
<x-slot:actions>
<x-button class="btn-outline" label="Skip for Now" wire:click="skipForNow" />
<x-button class="btn-primary" label="Set Picture" wire:click="setPicture" />
</x-slot:actions>
</x-form>
</div>
</div>
<x-slot:actions>
<x-button class="btn-outline" label="Skip for Now" wire:click="skipForNow" />
<x-button class="btn-primary" label="Next" wire:click="continue" />

</x-slot:actions>
</x-card>
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,3 @@
</ul>
</div>
</div>

1 change: 0 additions & 1 deletion resources/views/test.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
$user->is_super_admin = true;
@endphp
</x-main-layout>

1 change: 1 addition & 0 deletions tests/Browser/Components/Sidebar.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function assert(Browser $browser): void
public function elements(): array
{
return [
'@profile-avatar' => '@profile-avatar',
'@theme-controller' => '@theme-controller',
'@next-7-days' => '@next-7-days',
'@logout' => '@logout',
Expand Down
68 changes: 64 additions & 4 deletions tests/Browser/RegisterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,94 @@

namespace Tests\Browser;

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\URL;
use Laravel\Dusk\Browser;
use Tests\Browser\Components\Sidebar;
use Tests\DuskTestCase;

class RegisterTest extends DuskTestCase
{
use DatabaseTruncation;

protected $userEmail = 'marcos@marcos.com'; // Store email as a class property

/**
* A basic browser test example.
* Common registration method
*/
public function test_user_can_register(): void
protected function register_user_and_get_verification_url(): string
{
$this->browse(function (Browser $browser) {
$browser
->visit('/register')
->type('@register-user-name', 'randomUser239')
->type('@register-full-name', 'Marcos Aparicio')
->type('@register-email', 'marcos@marcos.com')
->type('@register-email', $this->userEmail) // Use class-level email
->type('@register-password', 'password')
->type('@register-confirm-password', 'password')
->press('Register')
->pause(2000);
$browser->assertSee('Resend Verification Email');
$this->assertDatabaseHas('users', [
'email' => 'marcos@marcos.com',
'email' => $this->userEmail, // Use class-level email
'email_verified_at' => null,
]);
});

// Retrieve the newly created user
$user = User::where('email', $this->userEmail)->firstOrFail(); // Use class-level email

// Generate the email verification URL directly from the user
$verificationUrl = URL::signedRoute('verification.verify', [
'id' => $user->id,
'hash' => sha1($user->email),
]);

// Ensure we got the verification URL
$this->assertNotEmpty($verificationUrl, 'Failed to generate verification URL.');

return $verificationUrl;
}

public function test_user_can_register_without_setting_profile_picture(): void
{
$verificationUrl = $this->register_user_and_get_verification_url();
// Now visit the email verification link, which will redirect to the set profile picture component
// skip the selection
$this->browse(function (Browser $browser) use ($verificationUrl) {
$browser
->visit($verificationUrl)
->press('Skip for Now')
->pause(1000)
->assertUrlIs(route('inbox'));
});
}

public function test_user_can_register_while_setting_profile_picture(): void
{
$verificationUrl = $this->register_user_and_get_verification_url();
$user = User::where('email', $this->userEmail)->firstOrFail(); // Use class-level email

$fakePic = UploadedFile::fake()->image('test_image.jpg', 200, 200);
$this->browse(function (Browser $browser) use ($verificationUrl, $fakePic, $user) {
$browser
->visit($verificationUrl)
->attach(
'profile-picture',
$fakePic->getPathname(),
)
->pause(1400)
->assertVisible('img')
->press('Set Picture')
->pause(1000)
->assertUrlIs(route('inbox'))
->within(new Sidebar, function (Browser $browser) use ($user) {
$user = User::where('email', $user->email)->firstOrFail();
$browser
->assertAttribute('@profile-avatar img', 'src', $user->getProfilePictureUrl());
});
});
}
}
19 changes: 0 additions & 19 deletions tests/Feature/ExampleTest.php

This file was deleted.

Loading