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
11 changes: 1 addition & 10 deletions app/Http/Controllers/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,7 @@ public function show($id)

return view('project', [
'project' => $project,
'boards' => $project->boards()
->visible()
->with(['items' => function ($query) {
return $query
->orderBy('pinned', 'desc')
->visibleForCurrentUser()
->popular() // TODO: This needs to be fixed to respect the sorting setting from the board itself (sort_items_by)
->withCount('votes');
}])
->get(),
'boards' => $project->boards()->visible()->get(),
]);
}
}
55 changes: 55 additions & 0 deletions app/Livewire/Project/BoardColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Livewire\Project;

use App\Models\Board;
use App\Models\Project;
use Livewire\Component;

class BoardColumn extends Component
{
public Project $project;
public Board $board;
public string $sortBy = 'created_at';
public string $search = '';

protected $listeners = [
'item-created' => '$refresh',
];

public function mount(): void
{
$this->sortBy = 'created_at';
}

public function setSortBy(string $sort): void
{
if (! in_array($sort, ['created_at', 'total_votes', 'last_commented'])) {
return;
}

$this->sortBy = $sort;
}

public function render()
{
$query = $this->board->items()
->visibleForCurrentUser()
->when($this->search, fn ($q) => $q->where('title', 'like', '%' . $this->search . '%'));

if ($this->sortBy === 'last_commented') {
$query->withMax('comments', 'created_at')
->orderBy('pinned', 'desc')
->orderByDesc('comments_max_created_at');
} else {
$query->orderBy('pinned', 'desc')
->orderByDesc($this->sortBy);
}

$items = $query->get();

return view('livewire.project.board-column', [
'items' => $items,
]);
}
}
6 changes: 6 additions & 0 deletions lang/en/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@

'close' => 'Close',
'save' => 'Save',

'search-items' => 'Search items...',
'sort-by' => 'Sort by',
'sort-newest' => 'Newest',
'sort-most-voted' => 'Most voted',
'sort-last-commented' => 'Last commented',
];
6 changes: 6 additions & 0 deletions lang/es/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@

'close' => 'Cerrar',
'save' => 'Guardar',

'search-items' => 'Buscar elementos...',
'sort-by' => 'Ordenar por',
'sort-newest' => 'Más recientes',
'sort-most-voted' => 'Más votados',
'sort-last-commented' => 'Último comentado',
];
6 changes: 6 additions & 0 deletions lang/hu/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,11 @@
'close' => 'Bezárás',
'save' => 'Mentés',

'search-items' => 'Elemek keresése...',
'sort-by' => 'Rendezés',
'sort-newest' => 'Legújabb',
'sort-most-voted' => 'Legtöbb szavazat',
'sort-last-commented' => 'Utoljára hozzászólt',

'public-user' => 'Nyilvános felhasználó',
];
6 changes: 6 additions & 0 deletions lang/it/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@
'close' => 'Chiudi',
'save' => 'Salva',

'search-items' => 'Cerca elementi...',
'sort-by' => 'Ordina per',
'sort-newest' => 'Più recenti',
'sort-most-voted' => 'Più votati',
'sort-last-commented' => 'Ultimo commento',

'public-user' => 'Utente pubblico',
];
6 changes: 6 additions & 0 deletions lang/nl/general.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@

'close' => 'Sluiten',
'save' => 'Opslaan',

'search-items' => 'Zoek items...',
'sort-by' => 'Sorteren op',
'sort-newest' => 'Nieuwste',
'sort-most-voted' => 'Meeste stemmen',
'sort-last-commented' => 'Laatst becommentarieerd',
];
128 changes: 128 additions & 0 deletions resources/views/livewire/project/board-column.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<section class="h-full">
<div class="bg-gray-100 rounded-xl min-w-[18rem] lg:w-92 flex flex-col max-h-full dark:bg-white/5">
<div
class="p-2 font-semibold border-b border-gray-200 bg-gray-100/80 rounded-t-xl backdrop-blur-xl backdrop-saturate-150 dark:bg-gray-900 dark:text-white dark:border-b-gray-800">
<div class="flex items-center justify-between">
<a
href="{{ route('projects.boards.show', [$project, $board]) }}"
class="border-b border-dotted border-black text-gray-800 dark:text-white">
{{ $board->title }}
</a>

<div x-data="{ open: false }" @click.away="open = false" class="relative">
<button
type="button"
@click="open = !open"
class="flex items-center justify-center w-8 h-8 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-200 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition"
>
<x-heroicon-o-adjustments-vertical class="w-4 h-4" />
</button>

<div
x-show="open"
x-cloak
class="absolute right-0 z-50 mt-1 w-52 origin-top-right rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10"
>
<div class="p-2">
<input
type="text"
wire:model.live.debounce.300ms="search"
placeholder="{{ trans('general.search-items') }}"
class="w-full rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 px-3 py-1.5 text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-brand-500 focus:border-brand-500"
/>
</div>
<div class="border-t border-gray-100 dark:border-gray-700">
<div class="py-1">
<button
type="button"
wire:click="setSortBy('created_at')"
@click="open = false"
@class([
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
'text-brand-500 font-medium' => $sortBy === 'created_at',
'text-gray-700 dark:text-gray-300' => $sortBy !== 'created_at',
])
>
{{ trans('general.sort-newest') }}
</button>
<button
type="button"
wire:click="setSortBy('total_votes')"
@click="open = false"
@class([
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
'text-brand-500 font-medium' => $sortBy === 'total_votes',
'text-gray-700 dark:text-gray-300' => $sortBy !== 'total_votes',
])
>
{{ trans('general.sort-most-voted') }}
</button>
<button
type="button"
wire:click="setSortBy('last_commented')"
@click="open = false"
@class([
'flex w-full items-center gap-2 px-4 py-2 text-sm whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-700',
'text-brand-500 font-medium' => $sortBy === 'last_commented',
'text-gray-700 dark:text-gray-300' => $sortBy !== 'last_commented',
])
>
{{ trans('general.sort-last-commented') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<ul class="p-2 space-y-2 o overflow-y-auto flex-1 min-h-0">
@forelse($items as $item)
<li>
<a href="{{ route('projects.items.show', [$project, $item]) }}"
class="block p-4 space-y-4 bg-white shadow rounded-xl hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-950">
<div class="flex justify-between">
<p>
{{ $item->title }}
</p>

<div class="flex items-center">
@if($item->isPrivate())
<span x-data x-tooltip.raw="{{ trans('items.item-private') }}">
<x-heroicon-s-lock-closed class="text-gray-500 fill-gray-500 w-5 h-5" />
</span>
@endif
@if($item->isPinned())
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
x-data
x-tooltip.raw="{{ trans('items.item-pinned') }}"
class="text-gray-500 fill-gray-500">
<path
d="M15 11.586V6h2V4a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2h2v5.586l-2.707 1.707A.996.996 0 0 0 6 14v2a1 1 0 0 0 1 1h4v3l1 2 1-2v-3h4a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L15 11.586z"></path>
</svg>
@endif
</div>
</div>

<footer class="flex items-end justify-between">
<span
class="inline-flex items-center justify-center h-6 px-2 text-sm font-semibold tracking-tight text-gray-700 dark:text-gray-300 rounded-full bg-gray-50 dark:bg-gray-600">
{{ $item->created_at->isoFormat('ll') }}
</span>

<div class="text-gray-500 text-sm">
{{ $item->total_votes }} {{ trans_choice('messages.votes', $item->total_votes) }}
</div>
</footer>
</a>
</li>
@empty
<li>
<div
class="p-3 font-medium text-center text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-400 border-dashed rounded-xl opacity-70">
<p>{{ trans('items.no-items') }}</p>
</div>
</li>
@endforelse
</ul>
</div>
</section>
62 changes: 1 addition & 61 deletions resources/views/project.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,67 +18,7 @@
])
>
@forelse($boards as $board)
<section class="h-full">
<div class="bg-gray-100 rounded-xl min-w-[18rem] lg:w-92 flex flex-col max-h-full dark:bg-white/5">
<div
class="p-2 font-semibold text-center text-gray-800 border-b border-gray-200 bg-gray-100/80 rounded-t-xl backdrop-blur-xl backdrop-saturate-150 dark:bg-gray-900 dark:text-white dark:border-b-gray-800">
<a
href="{{ route('projects.boards.show', [$project, $board]) }}"
class="border-b border-dotted border-black">
{{ $board->title }}
</a>
</div>
<ul class="p-2 space-y-2 o overflow-y-auto flex-1 min-h-0">
@forelse($board->items as $item)
<li>
<a href="{{ route('projects.items.show', [$project, $item]) }}"
class="block p-4 space-y-4 bg-white shadow rounded-xl hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-950">
<div class="flex justify-between">
<p>
{{ $item->title }}
</p>

<div class="flex items-center">
@if($item->isPrivate())
<span x-data x-tooltip.raw="{{ trans('items.item-private') }}">
<x-heroicon-s-lock-closed class="text-gray-500 fill-gray-500 w-5 h-5" />
</span>
@endif
@if($item->isPinned())
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
x-data
x-tooltip.raw="{{ trans('items.item-pinned') }}"
class="text-gray-500 fill-gray-500">
<path
d="M15 11.586V6h2V4a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v2h2v5.586l-2.707 1.707A.996.996 0 0 0 6 14v2a1 1 0 0 0 1 1h4v3l1 2 1-2v-3h4a1 1 0 0 0 1-1v-2a.996.996 0 0 0-.293-.707L15 11.586z"></path>
</svg>
@endif
</div>
</div>

<footer class="flex items-end justify-between">
<span
class="inline-flex items-center justify-center h-6 px-2 text-sm font-semibold tracking-tight text-gray-700 dark:text-gray-300 rounded-full bg-gray-50 dark:bg-gray-600">
{{ $item->created_at->isoFormat('ll') }}
</span>

<div class="text-gray-500 text-sm">
{{ $item->total_votes }} {{ trans_choice('messages.votes', $item->total_votes) }}
</div>
</footer>
</a>
</li>
@empty
<li>
<div
class="p-3 font-medium text-center text-gray-500 dark:text-gray-400 border border-gray-300 dark:border-gray-400 border-dashed rounded-xl opacity-70">
<p>{{ trans('items.no-items') }}</p>
</div>
</li>
@endforelse
</ul>
</div>
</section>
<livewire:project.board-column :project="$project" :board="$board" :key="$board->id" />
@empty
<div class="w-full">
<div
Expand Down
14 changes: 8 additions & 6 deletions tests/Feature/Controllers/ProjectControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
it('includes the items', function () {
$project = Project::factory()
->has(
Board::factory(5)
->has(Item::factory(10))
Board::factory()
->has(Item::factory(3))
)
->create();

get(route('projects.show', $project))->assertSeeInOrder(Item::all()->pluck('title')->toArray());
$response = get(route('projects.show', $project));

Item::all()->each(fn (Item $item) => $response->assertSeeText($item->title));
});

it('includes votes for items', function () {
Expand Down Expand Up @@ -71,14 +73,14 @@
get(route('projects.show', $project))->assertSeeInOrder(['item 2' , 'item 1']);
});

test('items are sorted by vote count', function () {
test('items are sorted by newest by default', function () {
$project = Project::factory()
->has(
Board::factory()
->has(
Item::factory(2)->state(new Sequence(
['title' => 'item 1', 'total_votes' => 1],
['title' => 'item 2', 'total_votes' => 10]
['title' => 'item 1', 'created_at' => now()->subDay()],
['title' => 'item 2', 'created_at' => now()]
))
)
)->create();
Expand Down
Loading
Loading