updated projects component

This commit is contained in:
liyunze 2026-09-04 11:22:58 +08:00
parent 09c4c24d77
commit abbed1d6b4
4 changed files with 146 additions and 139 deletions

View file

@ -26,6 +26,35 @@ const {
} = Astro.props; } = Astro.props;
const leftProject = position == "left"; const leftProject = position == "left";
const side = leftProject ? "left" : "right";
const layers = ["background", "frontfacing"] as const;
const renderLayer = (
src: ImageMetadata,
theme: "dark" | "light",
layer: (typeof layers)[number],
) => {
const isBackground = layer === "background";
const isLight = theme === "light";
return {
src,
class: [
"absolute top-1/2 left-1/2 aspect-video max-w-full rounded-xl border-2 border-solid",
isLight
? "project-image-light hidden border-gray-300"
: "project-image-dark border-gray-800",
isBackground ? "background-img opacity-40" : "frontfacing-img opacity-100",
isBackground && isLight ? "brightness-75" : "",
side,
]
.filter(Boolean)
.join(" "),
classList: {
"has-light-image": !isLight && Boolean(image_light),
},
};
};
--- ---
<div <div
@ -37,48 +66,34 @@ const leftProject = position == "left";
> >
<slot /> <slot />
{ {
image && alt && ( image &&
<> layers.map((layer) => {
<Image const layerProps = renderLayer(image, "dark", layer);
src={image} return (
alt={alt} <Image
class="project-image-dark background-img absolute top-1/2 left-1/2 aspect-video max-w-full rounded-xl border-2 border-solid border-gray-800 opacity-40" src={layerProps.src}
class:list={[ alt={alt ?? ""}
leftProject ? "left" : "right", width={700}
{ "has-light-image": Boolean(image_light) }, class={layerProps.class}
]} class:list={layerProps.classList}
width={700} />
/> );
<Image })
src={image} }
alt={alt} {
width={700} image_light &&
class="project-image-dark frontfacing-img absolute top-1/2 left-1/2 aspect-video max-w-full rounded-xl border-2 border-solid border-gray-800 opacity-100" layers.map((layer) => {
class:list={[ const layerProps = renderLayer(image_light, "light", layer);
leftProject ? "left" : "right", return (
{ "has-light-image": Boolean(image_light) }, <Image
]} src={layerProps.src}
/> alt={alt ?? ""}
{image_light && ( width={700}
<> class={layerProps.class}
<Image class:list={layerProps.classList}
src={image_light} />
alt={alt} );
class="project-image-light background-img absolute top-1/2 left-1/2 hidden aspect-video max-w-full rounded-xl border-2 border-solid border-gray-300 opacity-40" })
class:list={leftProject ? "left" : "right"}
width={700}
/>
<Image
src={image_light}
alt={alt}
width={700}
class="project-image-light frontfacing-img absolute top-1/2 left-1/2 hidden aspect-video max-w-full rounded-xl border-2 border-solid border-gray-300 opacity-100"
class:list={leftProject ? "left" : "right"}
/>
</>
)}
</>
)
} }
<div <div

View file

@ -62,26 +62,7 @@ import Project from "./Project.astro";
endless!`} endless!`}
tags={["HTML", "CSS", "Javascript", "C#"]} tags={["HTML", "CSS", "Javascript", "C#"]}
> >
<TaskWidget <TaskWidget position="right" client:visible />
classList={`
widget h-full
[perspective:200px] absolute
w-full
opacity-10
top-1/2 left-1/2
background-widget right
`}
client:visible
/>
<TaskWidget
classList={`
widget h-full
[perspective:200px] absolute
frontfacing-widget right
top-1/2 left-1/2
w-full`}
client:visible
/>
</Project> </Project>
</div> </div>
</div> </div>

View file

@ -135,74 +135,83 @@
]; ];
interface Props { interface Props {
classList: string; position?: "left" | "right";
} }
let { classList }: Props = $props(); let { position = "right" }: Props = $props();
const side = $derived(position === "left" ? "left" : "right");
</script> </script>
<div class={`${classList} overflow-hidden`}> {#snippet widgetLayer(layerClass: string, extraClass: string)}
<!-- task bot --> <div
<div class="task-widget absolute flex h-full min-h-[50vh] w-full flex-col"> class={`widget absolute top-1/2 left-1/2 h-full w-full overflow-hidden [perspective:200px] ${layerClass} ${side} ${extraClass}`}
>
<div <div
class="task-widget-header flex w-full flex-row items-center justify-between class="task-widget absolute flex h-full min-h-[50vh] w-full flex-col"
>
<div
class="task-widget-header flex w-full flex-row items-center justify-between
rounded-md rounded-md
border-3 border-solid border-3 border-solid
border-gray-600 bg-black border-gray-600 bg-black
px-5 py-2 font-['Fredoka',_sans-serif] text-sm text-white px-5 py-2 font-['Fredoka',_sans-serif] text-sm text-white
lg:text-xl" lg:text-xl"
> >
<span>Hover here!</span> <span>Hover here!</span>
<span>0/100+</span> <span>0/100+</span>
</div>
<div
class="scroll-parent relative h-[82%] overflow-hidden rounded-br-2xl rounded-bl-2xl font-['Fredoka',_sans-serif] lg:min-h-0"
>
<div class="scroll-element primary absolute w-full px-1">
{#each streamers as { streamer, tasks }}
<div
class="task-widget-item my-2 w-full overflow-hidden rounded-md bg-[#404040] px-5 py-1
text-sm overflow-ellipsis whitespace-nowrap text-white lg:text-base"
>
{streamer} :
<ol class="list-inside list-decimal">
{#each tasks as task}
<li
class={task.completed
? "task-completed text-gray-400 line-through"
: "task-incomplete text-white"}
>
{task.name}
</li>
{/each}
</ol>
</div>
{/each}
</div> </div>
<div class="scroll-element secondary absolute w-full px-1"> <div
{#each streamers as { streamer, tasks }} class="scroll-parent relative h-[82%] overflow-hidden rounded-br-2xl rounded-bl-2xl font-['Fredoka',_sans-serif] lg:min-h-0"
<div >
class="task-widget-item my-2 w-full overflow-hidden rounded-md bg-[#404040] px-5 py-1 <div class="scroll-element primary absolute w-full px-1">
{#each streamers as { streamer, tasks }}
<div
class="task-widget-item my-2 w-full overflow-hidden rounded-md bg-[#404040] px-5 py-1
text-sm overflow-ellipsis whitespace-nowrap text-white lg:text-base" text-sm overflow-ellipsis whitespace-nowrap text-white lg:text-base"
> >
{streamer} : {streamer} :
<ol class="list-inside list-decimal"> <ol class="list-inside list-decimal">
{#each tasks as task} {#each tasks as task}
<li <li
class={task.completed class={task.completed
? "task-completed line-through" ? "task-completed text-gray-400 line-through"
: "task-incomplete"} : "task-incomplete text-white"}
> >
{task.name} {task.name}
</li> </li>
{/each} {/each}
</ol> </ol>
</div> </div>
{/each} {/each}
</div>
<div class="scroll-element secondary absolute w-full px-1">
{#each streamers as { streamer, tasks }}
<div
class="task-widget-item my-2 w-full overflow-hidden rounded-md bg-[#404040] px-5 py-1
text-sm overflow-ellipsis whitespace-nowrap text-white lg:text-base"
>
{streamer} :
<ol class="list-inside list-decimal">
{#each tasks as task}
<li
class={task.completed
? "task-completed line-through"
: "task-incomplete"}
>
{task.name}
</li>
{/each}
</ol>
</div>
{/each}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {/snippet}
{@render widgetLayer("background-widget", "opacity-40 brightness-75")}
{@render widgetLayer("frontfacing-widget", "opacity-100")}
<style> <style>
:global(html[data-theme="light"]) .task-widget-header { :global(html[data-theme="light"]) .task-widget-header {

View file

@ -21,7 +21,8 @@
tags = [], tags = [],
}: Props = $props(); }: Props = $props();
const positionLeft: boolean = (() => position == "left")(); const positionLeft = $derived(position === "left");
const side = $derived(positionLeft ? "left" : "right");
onMount(() => { onMount(() => {
const videos = document.querySelectorAll<HTMLVideoElement>(".playable"); const videos = document.querySelectorAll<HTMLVideoElement>(".playable");
@ -63,30 +64,31 @@
<div <div
class="image-container relative mb-5 flex aspect-video w-full [perspective:200px] lg:mr-5 lg:mb-0 lg:ml-auto lg:w-1/2" class="image-container relative mb-5 flex aspect-video w-full [perspective:200px] lg:mr-5 lg:mb-0 lg:ml-auto lg:w-1/2"
> >
<img {#snippet renderLayer(
src={image_src} kind: "image" | "video",
alt={image_alt} layerClass: string,
class="project-media background-img absolute extraClass: string,
top-1/2 )}
left-1/2 max-w-full rounded-xl border-2 {#if kind === "image"}
border-solid border-gray-800 <img
opacity-40 src={image_src}
{positionLeft ? 'left' : 'right'}" alt={image_alt}
width="700" class="project-media absolute top-1/2 left-1/2 max-w-full rounded-xl border-2 border-solid border-gray-800 {layerClass} {extraClass} {side}"
/> width="700"
/>
{:else}
<!-- svelte-ignore a11y_media_has_caption -->
<video
width="700"
class="project-media playable absolute top-1/2 left-1/2 max-w-full rounded-xl border-2 border-solid border-gray-800 {layerClass} {extraClass} {side}"
>
<source src={video_src} type="video/webm" />
</video>
{/if}
{/snippet}
<!-- svelte-ignore a11y_media_has_caption --> {@render renderLayer("image", "background-img", "opacity-40 brightness-75")}
<video {@render renderLayer("video", "frontfacing-vid", "opacity-100")}
width="700"
class="project-media playable frontfacing-vid absolute
top-1/2 left-1/2 max-w-full rounded-xl
border-2
border-solid border-gray-800
opacity-100
{positionLeft ? 'left' : 'right'}"
>
<source src={video_src} type="video/webm" />
</video>
</div> </div>
<div <div
class="flex h-fit w-full flex-col items-center justify-center lg:h-[340px] lg:w-1/2 lg:px-10 class="flex h-fit w-full flex-col items-center justify-center lg:h-[340px] lg:w-1/2 lg:px-10