Remove format.ts and changes the layout to fit on small and big screens
This commit is contained in:
11
src/app.css
11
src/app.css
@@ -46,6 +46,17 @@
|
|||||||
transform: rotateY(2.4deg);
|
transform: rotateY(2.4deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--animate-pulse-ring: pulse-ring 1.5s ease-in-out infinite;
|
||||||
|
@keyframes pulse-ring {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-ecsess-600/50 relative aspect-square size-20 overflow-hidden rounded-full sm:size-24"
|
class="bg-ecsess-600 relative aspect-square size-24 overflow-hidden rounded-full sm:size-24"
|
||||||
>
|
>
|
||||||
{#if src && !imageError}
|
{#if src && !imageError}
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,60 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { onViewProfile, name, position, image, featured = false, tag = null } = $props();
|
let { onViewProfile, name, position, image, featured = false, tag = null } = $props();
|
||||||
import { getInitials } from '$lib/format';
|
|
||||||
|
function getInitials(n: string | null | undefined): string {
|
||||||
|
if (n == null || typeof n !== 'string') return '';
|
||||||
|
return n
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((w) => w.charAt(0).toUpperCase())
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
let imageError = $state(false);
|
let imageError = $state(false);
|
||||||
|
|
||||||
function handleImageError() {
|
function handleImageError() {
|
||||||
imageError = true;
|
imageError = true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
|
<!-- Wrapper: card + ring layer (ring pulses on small screen only) -->
|
||||||
<article
|
<div class="relative w-full max-w-full sm:contents">
|
||||||
class="group bg-ecsess-800 ring-ecsess-500/50 ring-offset-ecsess-900 hover:ring-ecsess-300 hover:shadow-ecsess-400/50 focus-within:ring-ecsess-300 focus-within:ring-offset-ecsess-900 grid h-full min-w-0 cursor-pointer grid-rows-[auto_1fr] overflow-hidden rounded-xl shadow-md ring-2 ring-offset-2 transition-all duration-500 ease-out focus-within:ring-2 focus-within:ring-offset-2 hover:shadow-xl {featured
|
|
||||||
? 'w-full max-w-56 sm:max-w-64'
|
|
||||||
: 'w-full max-w-56 sm:max-w-64'}"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick={onViewProfile}
|
|
||||||
onkeydown={(e) =>
|
|
||||||
e.key === 'Enter' || e.key === ' ' ? (e.preventDefault(), onViewProfile()) : null}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="bg-ecsess-700 relative flex aspect-square min-h-0 w-full items-center justify-center overflow-hidden"
|
class="group relative flex h-full min-w-0 cursor-pointer flex-row overflow-hidden rounded-xl shadow-md transition-all duration-500 ease-out sm:grid sm:grid-rows-[auto_1fr] {featured
|
||||||
|
? 'w-full sm:max-w-64 md:max-w-[13.6rem] lg:max-w-72'
|
||||||
|
: 'w-full sm:max-w-64 md:max-w-[13.6rem] lg:max-w-72'} bg-ecsess-800 ring-0 ring-offset-0 ring-ecsess-500/50 ring-offset-ecsess-900 hover:ring-ecsess-300 hover:shadow-ecsess-400/50 focus-within:ring-ecsess-300 focus-within:ring-offset-ecsess-900 focus-within:ring-2 focus-within:ring-offset-2 hover:shadow-xl sm:ring-2 sm:ring-offset-2"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={onViewProfile}
|
||||||
|
onkeydown={(e) =>
|
||||||
|
e.key === 'Enter' || e.key === ' ' ? (e.preventDefault(), onViewProfile()) : null}
|
||||||
>
|
>
|
||||||
{#if tag}
|
{#if tag}
|
||||||
<span
|
<span
|
||||||
class="bg-ecsess-600 text-ecsess-50 ring-ecsess-400/60 absolute top-2 right-2 z-1 rounded-md px-2 py-1 text-xs font-semibold tracking-wide uppercase shadow-md ring-1"
|
class="absolute top-2 right-2 z-10 rounded-md px-2 py-1 text-xs font-semibold uppercase tracking-wide shadow-md ring-1 bg-ecsess-600 text-ecsess-50 ring-ecsess-400/60"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if image && !imageError}
|
|
||||||
<img
|
<!-- Picture section: fills entire area, image covers it -->
|
||||||
src={image}
|
<div
|
||||||
alt={`Profile picture of ${name} for the position of ${position}`}
|
class="relative flex min-w-20 shrink-0 items-center justify-center overflow-hidden rounded-l-xl bg-ecsess-700 aspect-square sm:min-w-0 sm:w-full sm:rounded-none"
|
||||||
class="h-full w-full object-cover object-center"
|
|
||||||
onerror={handleImageError}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<span class="text-ecsess-150 text-4xl font-bold sm:text-5xl" aria-hidden="true">
|
|
||||||
{getInitials(name)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center justify-center gap-2 px-2 py-3 text-center sm:px-3 sm:py-4"
|
|
||||||
>
|
|
||||||
<h3 class="text-ecsess-50 line-clamp-2 w-full text-base leading-tight font-bold sm:text-lg">
|
|
||||||
{name}
|
|
||||||
</h3>
|
|
||||||
<p class="text-ecsess-200 line-clamp-2 w-full text-xs italic sm:text-sm">{position}</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="bg-ecsess-500 text-ecsess-950 hover:bg-ecsess-400 hover:text-ecsess-50 focus:ring-ecsess-300 focus:ring-offset-ecsess-800 mt-1 w-full cursor-pointer rounded-lg px-3 py-2 text-xs font-semibold shadow-md transition focus:ring-2 focus:ring-offset-2 focus:outline-none sm:text-sm lg:hidden"
|
|
||||||
>
|
>
|
||||||
View profile
|
{#if image && !imageError}
|
||||||
</button>
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`Profile picture of ${name} for the position of ${position}`}
|
||||||
|
class="size-full object-cover object-center"
|
||||||
|
onerror={handleImageError}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<span class="text-ecsess-150 text-2xl font-bold sm:text-5xl" aria-hidden="true">
|
||||||
|
{getInitials(name)}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex min-w-0 flex-1 flex-col justify-center gap-1 px-3 py-2 text-left sm:items-center sm:justify-center sm:gap-2 sm:px-3 sm:py-4 sm:text-center"
|
||||||
|
>
|
||||||
|
<h3 class="w-full text-lg font-bold leading-tight text-ecsess-50 line-clamp-2 sm:text-lg">
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<p class="w-full text-xs italic text-ecsess-200 line-clamp-2 sm:text-sm">{position}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-1 hidden w-full rounded-lg bg-ecsess-500 px-3 py-2 text-xs font-semibold text-ecsess-950 shadow-md transition focus:ring-2 focus:ring-ecsess-300 focus:ring-offset-2 focus:ring-offset-ecsess-800 focus:outline-none hover:bg-ecsess-400 hover:text-ecsess-50 sm:mt-1 sm:block lg:hidden"
|
||||||
|
>
|
||||||
|
View profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
|
<!-- Small screen only: pulsing ring (opacity only), not the card -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 pointer-events-none rounded-xl ring-2 ring-ecsess-400/60 ring-offset-2 ring-offset-ecsess-900 animate-pulse-ring sm:hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,13 @@
|
|||||||
onClose,
|
onClose,
|
||||||
id = 'popup-title'
|
id = 'popup-title'
|
||||||
} = $props();
|
} = $props();
|
||||||
import { getInitials } from '$lib/format';
|
|
||||||
import { Mail, X } from '@lucide/svelte';
|
import { Mail, X } from '@lucide/svelte';
|
||||||
|
|
||||||
|
function getInitials(name: string | null | undefined): string {
|
||||||
|
if (name == null || typeof name !== 'string') return '';
|
||||||
|
const words = name.trim().split(/\s+/).filter(Boolean);
|
||||||
|
return words.slice(0, 3).map((w) => w.charAt(0).toUpperCase()).join('');
|
||||||
|
}
|
||||||
import { scale } from 'svelte/transition';
|
import { scale } from 'svelte/transition';
|
||||||
|
|
||||||
let imageError = $state(false);
|
let imageError = $state(false);
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Get initials from a name (e.g. "Jane Doe" → "JD", "Mary Jane Watson" → "MJW").
|
|
||||||
* Uses first letter of up to 3 words. Safe for null/undefined/empty.
|
|
||||||
*/
|
|
||||||
export function getInitials(name: string | null | undefined): string {
|
|
||||||
if (name == null || typeof name !== 'string') return '';
|
|
||||||
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
||||||
if (words.length === 0) return '';
|
|
||||||
return words
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((w) => w.charAt(0).toUpperCase())
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user