Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Contracts\ApiKeyClientContract;
use App\Contracts\OAuthClientContract;
use App\Enums\Concerns\StringifyEnum;
use App\Support\ProfileHandle;
use Filament\Support\Colors\Color;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasDescription;
Expand Down Expand Up @@ -106,6 +107,20 @@ public function getApiKeyClient(): ?ApiKeyClientContract
};
}

public function profileUrl(?string $handle): ?string
{
if (blank($handle)) {
return null;
}

return match ($this) {
self::GitHub => ProfileHandle::url('https://github.com/', $handle),
self::Twitch => ProfileHandle::url('https://twitch.tv/', $handle),
self::DevTo => ProfileHandle::url('https://dev.to/', $handle),
default => null,
};
}

public function getClient(): ?OAuthClientContract
{
return match ($this) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider;

it('builds a profile url for the providers that have one', function (IdentityProvider $provider, string $expected): void {
expect($provider->profileUrl('danielhe4rt'))->toBe($expected);
})->with([
'github' => [IdentityProvider::GitHub, 'https://github.com/danielhe4rt'],
'twitch' => [IdentityProvider::Twitch, 'https://twitch.tv/danielhe4rt'],
]);

it('returns null for discord, which has no public profile page', function (): void {
expect(IdentityProvider::Discord->profileUrl('danielhe4rt'))->toBeNull();
});

it('returns null for providers outside the supported set', function (): void {
expect(IdentityProvider::Steam->profileUrl('danielhe4rt'))->toBeNull()
->and(IdentityProvider::Spotify->profileUrl('danielhe4rt'))->toBeNull();
});

it('strips a leading @ and surrounding spaces from the handle', function (string $handle): void {
expect(IdentityProvider::GitHub->profileUrl($handle))->toBe('https://github.com/danielhe4rt');
})->with([
'plain' => ['danielhe4rt'],
'at prefixed' => ['@danielhe4rt'],
'padded' => [' danielhe4rt '],
'at prefixed and padded' => [' @danielhe4rt '],
]);

it('passes an already absolute url through untouched', function (): void {
expect(IdentityProvider::GitHub->profileUrl('https://github.com/danielhe4rt'))
->toBe('https://github.com/danielhe4rt');
});

it('returns null for an empty handle', function (?string $handle): void {
expect(IdentityProvider::GitHub->profileUrl($handle))->toBeNull();
})->with([
'null' => [null],
'empty' => [''],
'only spaces' => [' '],
]);

it('gives every supported provider a decided answer', function (): void {
$decided = [
IdentityProvider::GitHub->value => 'https://github.com/handle',
IdentityProvider::Twitch->value => 'https://twitch.tv/handle',
IdentityProvider::DevTo->value => 'https://dev.to/handle',
IdentityProvider::Discord->value => null,
];

foreach (IdentityProvider::supportedProviders() as $provider) {
expect($decided)->toHaveKey($provider->value)
->and($provider->profileUrl('handle'))->toBe($decided[$provider->value]);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
@props (['user' => null])

@php
$isLinkable = $user !== null && $user->banned_at === null && filled($user->username);
@endphp

@if ($isLinkable)
<span
class="contents"
x-data="{
url: @js(route('profile.card', $user->username)),
open: false,
loading: false,
html: null,
hoverable: window.matchMedia('(hover: hover)').matches,
cardStyle: '',
openTimer: null,
closeTimer: null,

scheduleOpen() {
if (! this.hoverable) return

clearTimeout(this.openTimer)
clearTimeout(this.closeTimer)
this.openTimer = setTimeout(() => this.show(), 400)
},

scheduleClose() {
clearTimeout(this.openTimer)
clearTimeout(this.closeTimer)
this.closeTimer = setTimeout(() => this.open = false, 300)
},

cancelClose() {
clearTimeout(this.closeTimer)
},

close() {
clearTimeout(this.openTimer)
clearTimeout(this.closeTimer)
this.open = false
},

position() {
if (! this.open) return

const trigger = this.$refs.trigger.getBoundingClientRect()
const width = this.$refs.card.offsetWidth || 320
const height = this.$refs.card.offsetHeight || 200
const left = Math.max(8, Math.min(trigger.left, window.innerWidth - width - 8))
const fitsBelow = trigger.bottom + 8 + height <= window.innerHeight - 8
const fitsAbove = trigger.top - 8 - height >= 8
const top = fitsBelow || ! fitsAbove ? trigger.bottom + 8 : trigger.top - 8 - height

this.cardStyle = 'left: ' + left + 'px; top: ' + Math.max(8, top) + 'px'
},

reposition() {
this.$nextTick(() => this.position())
},

async show() {
if (! this.hoverable) return

this.open = true
this.reposition()

if (this.html !== null) return

this.loading = true

const response = await fetch(this.url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
}).catch(() => null)

this.loading = false

if (! response || ! response.ok) {
this.open = false

return
}

this.html = await response.text()
this.reposition()
},
}"
x-on:keydown.escape.window="close()"
x-on:scroll.window.passive="reposition()"
x-on:resize.window.passive="reposition()"
>
<a
href="{{ route('profile.public', $user->username) }}"
x-ref="trigger"
x-on:mouseenter="scheduleOpen()"
x-on:mouseleave="scheduleClose()"
x-on:focus="show()"
x-on:blur="scheduleClose()"
{{ $attributes }}
>
{{ $slot }}
</a>

<div
x-ref="card"
x-cloak
x-show="open"
x-bind:style="cardStyle"
x-transition.opacity.duration.150ms
x-on:mouseenter="cancelClose()"
x-on:mouseleave="scheduleClose()"
class="fixed z-50 w-80 max-w-[calc(100vw-1rem)]"
>
<template x-if="loading">
<div
class="rounded-xl border border-gray-200 bg-white p-4 shadow-lg dark:border-white/10 dark:bg-gray-900"
>
<div class="flex items-start gap-3">
<div class="h-12 w-12 shrink-0 animate-pulse rounded-full bg-gray-200 dark:bg-white/10"></div>
<div class="flex-1 space-y-2 pt-1">
<div class="h-3 w-2/3 animate-pulse rounded bg-gray-200 dark:bg-white/10"></div>
<div class="h-2.5 w-1/3 animate-pulse rounded bg-gray-200 dark:bg-white/10"></div>
</div>
</div>
<div class="mt-4 space-y-2">
<div class="h-2.5 w-full animate-pulse rounded bg-gray-200 dark:bg-white/10"></div>
<div class="h-2.5 w-1/2 animate-pulse rounded bg-gray-200 dark:bg-white/10"></div>
</div>
</div>
</template>

<template x-if="html !== null">
<div x-html="html"></div>
</template>
</div>
</span>
@else
<span {{ $attributes }}>{{ $slot }}</span>
@endif
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,30 @@

@php
$displayName = $user?->name ?? 'Usuário removido';
$avatarUrl = $user?->getFirstMediaUrl('avatar') ?: null;
@endphp

<div class="flex items-center gap-3 px-3 pt-3 pb-2 sm:px-4 sm:pt-4">
@if ($avatarUrl)
<img
src="{{ $avatarUrl }}"
alt="{{ $displayName }}"
class="h-9 w-9 shrink-0 rounded-full object-cover sm:h-10 sm:w-10"
/>
@else
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-amber-500 text-xs font-semibold text-white sm:h-10 sm:w-10 sm:text-sm"
>
{{ str($displayName)->substr(0, 2)->upper() }}
</div>
@endif
<x-panel-app::profile-link :user="$user" class="shrink-0">
<x-panel-app::user-avatar :user="$user" size="lg" />
</x-panel-app::profile-link>

<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5">
<span class="truncate text-sm font-semibold text-gray-900 dark:text-white">{{ $displayName }}</span>
@if ($user?->username)
<x-panel-app::profile-link
:user="$user"
class="group flex min-w-0 items-center gap-x-2"
>
<span
class="hidden text-sm text-gray-500 sm:inline dark:text-gray-400"
>{{ '@' . $user->username }}</span
class="truncate text-sm font-semibold text-gray-900 group-hover:underline dark:text-white"
>{{ $displayName }}</span
>
@endif
@if ($user?->username)
<span
class="hidden text-sm text-gray-500 sm:inline dark:text-gray-400"
>{{ '@' . $user->username }}</span
>
@endif
</x-panel-app::profile-link>
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $createdAt->diffForHumans(short: true) }}</span>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@props (['user' => null, 'size' => 'md'])

@use('He4rt\Profile\Support\ProfileInitials')

@php
$displayName = $user?->name ?? 'Usuário removido';
$avatarUrl = $user?->getFirstMediaUrl('avatar') ?: null;
$initials = ProfileInitials::for($displayName, $user?->username);

$sizeClasses = match ($size) {
'lg' => 'h-9 w-9 sm:h-10 sm:w-10',
'sm' => 'h-7 w-7 sm:h-8 sm:w-8',
default => 'h-8 w-8',
};

$initialsClasses = match ($size) {
'lg' => 'text-xs sm:text-sm',
'sm' => 'text-[10px] sm:text-xs',
default => 'text-xs',
};
@endphp

@if ($avatarUrl)
<img
src="{{ $avatarUrl }}"
alt="{{ $displayName }}"
{{ $attributes->class(['shrink-0 rounded-full object-cover', $sizeClasses]) }}
/>
@else
<div
{{
$attributes->class([
'flex shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-amber-500 font-semibold text-white',
$sizeClasses,
$initialsClasses,
])
}}
>
{{ $initials }}
</div>
@endif
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,7 @@ class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-wh
>
<form wire:submit="post">
<div class="flex gap-2 px-3 pt-3 pb-2 sm:gap-3 sm:px-4 sm:pt-4 sm:pb-3">
@if ($this->avatarUrl)
<img
src="{{ $this->avatarUrl }}"
alt="{{ auth()->user()->name }}"
class="hidden h-10 w-10 shrink-0 rounded-full object-cover sm:block"
/>
@else
<div
class="hidden h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-amber-500 text-sm font-semibold text-white sm:flex"
>
{{
str(auth()->user()->name)
->substr(0, 2)
->upper()
}}
</div>
@endif
<x-panel-app::user-avatar :user="auth()->user()" size="lg" class="hidden sm:flex" />
<div class="[&_.fi-fo-field-wrp]:gap-0 [&_.fi-fo-field-wrp-label]:hidden min-w-0 flex-1">
{{ $this->form }}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,17 @@
@foreach ($timeline->children->take(3) as $reply)
@continue (!$reply->postable || !$reply->user)
<div class="flex gap-3">
@php($replyAvatarUrl = $reply->user->getFirstMediaUrl('avatar') ?: null)
@if ($replyAvatarUrl)
<img
src="{{ $replyAvatarUrl }}"
alt="{{ $reply->user->name }}"
class="h-8 w-8 shrink-0 rounded-full object-cover"
/>
@else
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-amber-500 text-xs font-semibold text-white"
>
{{ str($reply->user->name)->substr(0, 2)->upper() }}
</div>
@endif
<x-panel-app::profile-link :user="$reply->user" class="shrink-0">
<x-panel-app::user-avatar :user="$reply->user" />
</x-panel-app::profile-link>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 text-sm">
<span class="font-semibold text-gray-900 dark:text-white">{{ $reply->user->name }}</span>
<x-panel-app::profile-link :user="$reply->user" class="min-w-0">
<span
class="truncate font-semibold text-gray-900 hover:underline dark:text-white"
>{{ $reply->user->name }}</span
>
</x-panel-app::profile-link>
<span
class="text-xs text-gray-500"
>{{ $reply->created_at->diffForHumans(short: true) }}</span
Expand Down
Loading