This commit is contained in:
Ricocc
2025-11-06 23:01:29 +08:00
parent d24e03ce6a
commit 1434ce3517
28 changed files with 236 additions and 157 deletions

111
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,111 @@
# 贡献指南
感谢你对这个项目的关注!我们欢迎所有形式的贡献。
## 如何贡献
### 报告 Bug
如果你发现了 bug
1. 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues) 确认该 bug 尚未被报告
2. 如果是一个新的 bug请创建一个新的 Issue包含
- 清晰的问题描述
- 复现步骤
- 预期行为
- 实际行为
- 截图(如果有)
- 环境信息(浏览器、操作系统等)
### 提出新功能
如果你有改进建议或新功能想法:
1. 查看现有 Issues 确认没有类似提议
2. 创建一个新的 Feature Request Issue描述
- 功能描述
- 使用场景
- 可能的实现方案(可选)
### 提交代码
1. **Fork 项目**
```bash
git clone https://github.com/your-username/ricoui-portfolio.git
cd ricoui-portfolio
```
2. **创建分支**
```bash
git checkout -b feature/your-feature-name
# 或
git checkout -b fix/your-bug-fix
```
3. **安装依赖**
```bash
pnpm install
```
4. **进行修改**
- 编写代码
- 确保代码通过检查:`pnpm check`
- 测试你的修改
5. **提交更改**
```bash
git add .
git commit -m "feat: 添加新功能描述"
# 或
git commit -m "fix: 修复bug描述"
```
提交信息请遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
- `feat:` 新功能
- `fix:` Bug 修复
- `docs:` 文档更新
- `style:` 代码格式调整(不影响功能)
- `refactor:` 代码重构
- `perf:` 性能优化
- `test:` 测试相关
- `chore:` 构建/工具相关
6. **推送并创建 Pull Request**
```bash
git push origin feature/your-feature-name
```
然后在 GitHub 上创建 Pull Request。
## 代码规范
- 使用 Biome 进行代码检查和格式化
- 运行 `pnpm check` 确保代码符合规范
- 保持代码简洁、可读
- 添加必要的注释
## 开发环境
- Node.js >= 18
- pnpm (推荐) 或 npm/yarn
- 现代浏览器Chrome、Firefox、Safari、Edge
## 测试
在提交 PR 前,请确保:
- [ ] 代码通过 `pnpm check`
- [ ] 在本地运行 `pnpm dev` 测试正常
- [ ] 构建通过 `pnpm build`
- [ ] 测试了不同的浏览器和设备
## 问题?
如果遇到问题,可以:
- 查看 [Issues](https://github.com/ricocc/ricoui-portfolio/issues)
- 创建新的 Issue 提问
- 联系维护者hello@ricoui.com
再次感谢你的贡献!🎉

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -18,11 +18,11 @@
},
{
"name": "Luon Models",
"description": "Company Website",
"tags": ["Company","Website","Branding"],
"description": "Agency Company Website",
"tags": ["Agency","Website"],
"image": "/assets/works/luonmodels.jpg",
"video": "/assets/works/luonmodels.mp4",
"url": "https://luonmodels.netlify.app/"
"url": "/work/luonmodels"
},
{
"name": "Ricoui",

View File

@@ -79,9 +79,9 @@ const {title, description
<h3>Writing</h3>
<p>Style guides, design notes, and quick reads.</p>
<div class="mt-2 btn opacity-0 transition duration-300 ease-in-out">
<Button url="/blog" size="sm">
<!-- <Button url="/blog" size="sm">
Blog
</Button>
</Button> -->
</div>
</div>
<div class="explore-figure computer absolute right-[5%] sm:right-[-10%] md:right-[-7%] bottom-[14%] w-[120px] sm:w-[130px] md:w-[148px] h-auto">

View File

@@ -46,7 +46,7 @@ const currentSizeClasses = sizeClasses[size];
target={type === "disabled" ? undefined : target}
class={`${
type === "solid"
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-[#f3f5ff] dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]`
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-gradient-to-b from-white to-[#edf1fa] dark:from-[#15233b] dark:to-[#0f1b2d] border-[1px] border-primary/20 dark:border-[#243757] max-w-60 transition duration-400 ease-in-out hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:hover:shadow-[0px_6x_9px_0_rgba(59,123,217,0.25)]`
: type === "fill"
? `flex justify-center items-center flex-grow-0 flex-shrink-0 relative gap-1.5 ${currentSizeClasses.padding} rounded-xl bg-btn-primary dark:bg-btn-primary-dark border-[0] border-[rgba(138,127,255,0.2)] shadow-[0px_2px_3px_0_rgba(0,0,0,0.1)] max-w-60 transition duration-400 ease-in-out hover:bg-btn-primary-hover dark:hover:bg-btn-primary-dark-hover hover:translate-y-[-2px] hover:shadow-[0px_6px_9px_0_rgba(61,99,171,0.15)] dark:shadow-[0px_6px_9px_0_rgba(0,0,0,0.2)]`
: type === "disabled"
@@ -58,11 +58,11 @@ const currentSizeClasses = sizeClasses[size];
onclick={type === "disabled" ? "return false;" : undefined}
>
{type === "solid" ? (
<div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-[#6f6c8f] dark:text-[#c9d7f2]`}>
<div class={`flex gap-1 items-center justify-center ${currentSizeClasses.fontSize} font-medium text-center text-neutral-700 dark:text-neutral-300`}>
<slot />
</div>
) : type === "fill" ? (
<span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-white`}>
<span class={`flex gap-1 items-center justify-center ${size === "md" ? "text-sm" : currentSizeClasses.fontSize} font-medium text-center text-neutral-50`}>
<slot />
</span>
) : type === "disabled" ? (

View File

@@ -5,15 +5,3 @@
<img src="/assets/logo.png" alt="Logo" class="w-10 h-10 rounded-full" />
</a>
{/* <a
href="/"
class="h-5 text-base group relative z-30 flex items-center space-x-1.5 text-black dark:text-white font-semibold"
>
<span
class="text-xl -translate-y-0.5 group-hover:-rotate-12 group-hover:scale-[1.2] ease-in-out duration-300"
>✦</span
>
<!-- Logo Text -->
<span class="-translate-y-0.5"> aria</span>
</a> */}

View File

@@ -1,91 +0,0 @@
---
interface Props {
class?: string; // Class of the marquee
marqueeElements: number; // Number of elements
marqueeElementWidth: string; // Width of the marquee
marqueeElementWidthAuto?: boolean; // Is the width auto
marqueeElementWidthResponsive: string; // Width of the marquee
marqueePauseOnHover?: boolean; // Duration of the marquee
marqueeReverse?: "reverse" | "" | undefined; // Duration of the marquee
marqueeDuration?: string; // Duration of the marquee
}
let {
marqueeElements,
marqueeElementWidth,
marqueeElementWidthAuto = true,
marqueeElementWidthResponsive,
marqueePauseOnHover = false,
marqueeDuration = "50s",
marqueeReverse = "",
} = Astro.props;
// Remove value of if `marqueeElementWidth`, `marqueeElementWidthResponsive` marqueeElementWidthAuto true
if (marqueeElementWidthAuto) {
marqueeElementWidth = "";
marqueeElementWidthResponsive = "";
}
---
<div
class:list={[
"marquee",
{
"marquee-revers": marqueeReverse,
"marquee-pause-on-hover": marqueePauseOnHover,
},
]}
style={`--marquee-elements:${marqueeElements};--marquee-element-width-responsive:calc(${marqueeElementWidthResponsive} / var(--marquee-elements) * ${marqueeElements});--marquee-element-width:calc(${marqueeElementWidth} / var(--marquee-elements) * ${marqueeElements});${"--marquee-duration:" + marqueeDuration};--marquee-reverse:${marqueeReverse};`}>
<div
class:list={[
"marquee-content flex items-center",
{ "marquee-element-width-auto": marqueeElementWidthAuto },
Astro.props.class,
]}>
<slot />
</div>
</div>
<script>
function updateMarquee() {
const marqueeContents = document.querySelectorAll(
".marquee-element-width-auto",
) as NodeListOf<HTMLElement>;
marqueeContents.forEach((marqueeContent) => {
const images = marqueeContent.querySelectorAll("img");
images.forEach((img) => {
if (!img.complete) {
// wait for images to load
img.addEventListener("load", () => updateImageSize(img));
} else {
updateImageSize(img);
}
});
// update total width after images resized
const totalWidth = marqueeContent.scrollWidth;
marqueeContent.style.setProperty("--total-width", `${totalWidth / 2}px`);
});
}
function updateImageSize(img: HTMLImageElement) {
const computed = getComputedStyle(img);
const height = parseFloat(computed.height); // Tailwind-applied height
const aspectRatio = img.naturalWidth / img.naturalHeight;
const width = height * aspectRatio;
img.setAttribute("height", height.toString());
img.setAttribute("width", width.toString());
}
// Debounced resize handler
let resizeTimeout: number;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = window.setTimeout(updateMarquee, 100);
});
// Initial run
updateMarquee();
</script>

View File

@@ -12,10 +12,10 @@
</div>
<script>
import Matter from "matter-js"; // 修改为默认导入
import Matter from "matter-js";
(() => {
// 懒加载:当 .tricks-view 进入视口时初始化
const target = document.querySelector(".tricks-view");
if (!target) {
console.error("[Tricks] .tricks-view not found");
@@ -39,7 +39,7 @@ import Matter from "matter-js"; // 修改为默认导入
async function initTricksAnimation() {
const container = document.querySelector(".tool-stack-box") || document.querySelector(".matter-wrapper");
const canvasHost = document.querySelector(".tricks-canvas") as HTMLElement;
const domLayer = document.querySelector(".tricks-elements") as HTMLElement; // 添加类型断言
const domLayer = document.querySelector(".tricks-elements") as HTMLElement;
if (!container || !canvasHost || !domLayer) {
console.error(
@@ -48,36 +48,36 @@ import Matter from "matter-js"; // 修改为默认导入
return;
}
// 配置(调大元素尺寸 & 缩小边缘间隙)
const pad = 2; // 内边距,越小越贴边
const margin = 1; // 静态边界与容器边缘距离,越小越贴边
const count = 15; // 元素数量
const noRotate = false; // 不旋转可设为 true
const scaleFactor = 0.75; // 全局放大因子:>1 放大,<1 缩小
// Config (enlarge element size & reduce edge gaps)
const pad = 2; // Inner padding; smaller value sticks closer to edges
const margin = 1; // Distance between static boundaries and container edges; smaller value sticks closer
const count = 15; // Number of elements
const noRotate = false; // Set to true to disable rotation
const scaleFactor = 0.75; // Global scaling factor: >1 enlarge, <1 shrink
// 尺寸
// Dimensions
let { width: cw, height: ch } = container.getBoundingClientRect();
let W = Math.max(cw - pad, 100);
let H = Math.max(ch - pad, 100);
// 物理引擎
// Physics engine
const engine = Matter.Engine.create();
const world = engine.world;
// 鼠标拖拽(事件落在 canvasHost 上)
// Mouse drag (events fall on canvasHost)
const mouseConstraint = Matter.MouseConstraint.create(engine, {
mouse: Matter.Mouse.create(canvasHost),
constraint: { render: { visible: false }, stiffness: 1 },
});
Matter.World.add(world, mouseConstraint);
// 边界(地面与左右墙)
// Boundaries (ground and side walls)
let ground = Matter.Bodies.rectangle(W / 2, H - margin, W, 14, { isStatic: true });
let wallL = Matter.Bodies.rectangle(margin, H / 2, 14, H, { isStatic: true });
let wallR = Matter.Bodies.rectangle(W - margin, H / 2, 14, H, { isStatic: true });
Matter.World.add(world, [ground, wallL, wallR]);
// 图标路径(按需调整)
// Icon paths (adjust as needed)
const iconUrls = [
"/assets/stack/astro.png",
"/assets/stack/css.png",
@@ -95,12 +95,12 @@ import Matter from "matter-js"; // 修改为默认导入
"/assets/stack/vscode.png",
];
// 预加载图片,获取 naturalWidth/Height
// Preload images to obtain naturalWidth/Height
function loadImage(url: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.decoding = "async";
img.loading = "eager"; // 懒加载触发后尽快加载
img.loading = "eager"; // Load as soon as lazy-load is triggered
img.src = url;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image load failed: " + url));
@@ -114,10 +114,10 @@ import Matter from "matter-js"; // 修改为默认导入
console.error("[Tricks] image load failed:", e);
}
// 尺寸策略:显著增大目标面积区间(适配 720x480
// Sizing strategy: significantly increase target area range (adapted for 720x480)
function computeAreaRange() {
// 以容器面积为基准,提高图标面积比例
// 720*480 ≈ 345600;这里让单个图标面积大致在 1/20 ~ 1/10 容器面积之间
// Use container area as the base; increase icon area proportion
// 720*480 ≈ 345600; here a single icon area is roughly between 1/20 ~ 1/10 of the container area
const baseArea = W * H;
const areaMin = clamp(baseArea / 20, 12000, 30000);
const areaMax = clamp(baseArea / 10, 20000, 50000);
@@ -125,7 +125,7 @@ import Matter from "matter-js"; // 修改为默认导入
}
let { areaMin, areaMax } = computeAreaRange();
// 图标刚体类(矩形,尺寸基于 naturalWidth/Height
// Icon rigid body class (rectangle, size based on naturalWidth/Height)
class IconBody {
w: number;
h: number;
@@ -141,29 +141,29 @@ import Matter from "matter-js"; // 修改为默认导入
? img.naturalWidth / img.naturalHeight
: 1;
// 目标面积(随机落在区间内)
// Target area (random within range)
const A = randRange(areaMin, areaMax);
// 推导宽高:w = sqrt(A*r), h = w/r
// Derive width/height: w = sqrt(A*r), h = w/r
let w = Math.sqrt(A * r);
let h = w / r;
// 全局放大或缩小
// Global scale up or down
w *= scaleFactor;
h *= scaleFactor;
this.w = w;
this.h = h;
// 物理矩形刚体
// Physics rectangle body
this.body = Matter.Bodies.rectangle(x, y, this.w, this.h, {
restitution: 0.35,
friction: 0.1,
frictionAir: 0.02,
density: clamp((this.w * this.h) / 40000, 0.001, 0.02),
inertia: noRotate ? Infinity : undefined, // 不旋转:设置惯性无穷
inertia: noRotate ? Infinity : undefined, // No rotation: set infinite inertia
});
// DOM:容器 + img
// DOM: container + img
this.el = document.createElement("div");
this.el.className = "tricks-circle";
this.el.style.width = `${this.w}px`;
@@ -172,11 +172,11 @@ import Matter from "matter-js"; // 修改为默认导入
const node = img.cloneNode(true) as HTMLImageElement;
node.style.width = "100%";
node.style.height = "100%";
node.style.objectFit = "contain"; // "cover"
node.style.objectFit = "contain"; // or "cover"
node.alt = node.alt || "icon";
this.el.appendChild(node);
domLayer.appendChild(this.el); // 这里 domLayer 已经确保不为 null
domLayer.appendChild(this.el); // domLayer is ensured to be non-null here
}
update() {
@@ -190,7 +190,7 @@ import Matter from "matter-js"; // 修改为默认导入
}
}
// 创建图标刚体
// Create icon rigid bodies
const total = Math.min(iconsImgs.length, count);
const iconsBodies: IconBody[] = [];
for (let i = 0; i < total; i++) {
@@ -198,16 +198,16 @@ import Matter from "matter-js"; // 修改为默认导入
}
Matter.World.add(world, iconsBodies.map((it) => it.body));
// 启动引擎
// Start engine
const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);
// 每次物理步进后,同步 DOM
// After each physics step, sync DOM
Matter.Events.on(engine, "afterUpdate", () => {
iconsBodies.forEach((it) => it.update());
});
// 自适应尺寸:更新边界位置,并重新计算面积区间
// Responsive sizing: update boundary positions and recompute area range
window.addEventListener("resize", () => {
const rect = container.getBoundingClientRect();
W = Math.max(rect.width - pad, 100);
@@ -223,7 +223,7 @@ import Matter from "matter-js"; // 修改为默认导入
});
}
// 工具函数IIFE 外也可用)
// Utility functions (also usable outside the IIFE)
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v));
}

View File

@@ -36,9 +36,9 @@ if (typeof logo === 'string') {
data-aos-duration="500"
data-aos-once="true"
>
<div class="flex items-center gap-2 sm:gap-3 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]">
<div class="flex items-center gap-1 sm:gap-2 rounded-2xl bg-neutral-900/85 dark:bg-neutral-900/85 text-neutral-100 shadow-[0_6px_28px_rgba(0,0,0,0.25)] ring-1 ring-white/10 backdrop-blur-xl px-2 py-2 max-w-[92vw]">
<!-- Logo block -->
<div class="shrink-0 h-10 w-10 sm:h-11 sm:w-11 rounded-xl bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden">
<div class="shrink-0 h-10 w-10 sm:h-10 sm:w-10 rounded-full bg-neutral-800 grid place-items-center ring-1 ring-white/10 overflow-hidden">
{logoSrc ? (
<img src={logoSrc} alt="logo" class="h-full w-full object-cover" loading="lazy" />
) : (
@@ -47,7 +47,7 @@ if (typeof logo === 'string') {
</div>
<!-- Middle pills (tags + optional github) -->
<div class="flex items-center gap-2sm:gap-3 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1">
<div class="flex items-center gap-1 sm:gap-1 overflow-x-auto scrollbar-none max-w-[46vw] sm:max-w-[52vw] md:max-w-[56vw] pr-1">
{tags && tags.map((t) => (
<span class=" hidden font-light sm:inline-flex items-center h-10 px-3 sm:px-3 rounded-xl bg-neutral-800/80 ring-1 ring-white/10 text-neutral-300 text-sm whitespace-nowrap">
{t}
@@ -72,7 +72,7 @@ if (typeof logo === 'string') {
href={url}
target="_blank"
rel="nofollow noopener"
class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 font-medium hover:bg-yellow-200 transition-colors whitespace-nowrap"
class="inline-flex items-center h-10 sm:h-11 px-4 sm:px-5 rounded-xl bg-yellow-300 text-neutral-900 text-sm hover:bg-yellow-200 transition-colors whitespace-nowrap"
>
{visitLabel}
</a>

View File

@@ -3,18 +3,19 @@
const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://portfolio.ricoui.com/';
export const siteConfig = {
title: "Rico Portfolio",
title: "Ricoui Portfolio",
author: "Ricoui",
url: SITE_URL,
mail: "hello@ricoui.com",
resume: "#",
// resume add your resume file path here: /assets/resume.pdf
resume: "https://ricoui.com/",
utm: {
source: `${SITE_URL}`,
medium: "referral",
campaign: "navigation",
},
meta:{
title: "Rico Portfolio",
title: "Ricoui Portfolio",
description: "I'm Rico, a web designer passionate about both design and code. Currently developing a personal product for the design community.",
keywords: "web designer, portfolio, design, code, personal website",
image: `${SITE_URL}/og.jpg`,

View File

@@ -329,7 +329,7 @@ const filtered = headings.filter(h => h.depth <= 3);
data-aos-once="true"
>Let's Connect</h2>
<p
class="text-sm leading-6 text-neutral-600 dark:text-neutral-300"
class="text-sm leading-6 text-neutral-700 dark:text-neutral-300"
data-aos="fade-up-xs"
data-aos-delay="100"
data-aos-duration="500"
@@ -339,7 +339,7 @@ const filtered = headings.filter(h => h.depth <= 3);
href={siteConfig.social.twitter}
target="_blank"
class="text-indigo-500 underline">follow me on twitter</a
>, or you can send me an <a href="mailto:hello@ricouic.om" class="text-indigo-500 underline"
>, or you can send me an <a href="mailto:hello@ricoui.com" class=" underline"
>email</a
> and I'll be sure to get back to you.
</p>

View File

@@ -4,7 +4,7 @@ import PageHeader from "@/components/elements/PageHeader.astro";
import BlogSection from "@/components/sections/BlogSection.astro";
// 使用与分页页面相同的每页文章数量
const POSTS_PER_PAGE = 3;
const POSTS_PER_PAGE = 6;
// 博客首页显示第一页内容
const currentPage = 1;
---

View File

@@ -10,8 +10,20 @@ export const POSTS_PER_PAGE = 6;
// 生成分页路径
export async function getStaticPaths() {
const allPosts = await getCollection("post");
// 使用顶层定义的POSTS_PER_PAGE常量
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
// 按发布日期排序文章
let posts = allPosts.sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
);
// 查找特色文章,在列表页需要从主列表中移除
const featuredPost = posts.find((post) => post.data.featured);
if (featuredPost) {
posts = posts.filter((post) => post.data.featured !== true);
}
// 使用移除特色文章后的文章数量计算总页数
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE);
return Array.from({ length: totalPages }, (_, i) => {
const page = i + 1;

View File

@@ -0,0 +1,58 @@
---
import { Image } from "astro:assets";
import PostLayout from "@/layouts/PostLayout.astro";
import PageHeader from "@/components/elements/PageHeader.astro";
import ActionBar from "@/components/widgets/ActionBar.astro";
import SeparatorLine from "@/components/elements/SeparatorLine.astro";
//自动导入 Image 组件和图像
const allImages = await Astro.glob('../../assets/work/luonmodels/*.{jpg,png,webp}');
const logoUrl = "https://luonmodels.vercel.app/favicon.png";
---
<PostLayout
title="LUON - Agency Website Template"
description = 'LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios.',
keywords = 'LUONModels, Agency Website Template, Astro, Modeling Agency, Photographer, Studio',
>
<div class="work article">
<main class="work-wrapper">
<PageHeader
title="LUON - Agency Website Template"
tags={["Designer","Portfolio","Open Source"]}
className="mb-6 md:mb-8"
>
</PageHeader>
</section>
<div class="site-container m-auto">
<SeparatorLine />
</div>
<section class="article-content">
<div class="site-container mx-auto my-8 md:my-16 ">
<div>
<p>
LUONModels is a sleek and fluid Astro enterprise template specifically designed for modeling agencies, photographers, and studios. It aims to showcase people, photography, and information clearly and effectively, while also making it convenient to build event pages, content blogs, and various other specialized pages.
</p>
<p>Features</p>
<ul>
<li>✅ Responsive Design: Fully optimized for mobile and desktop viewing, ensuring a consistent experience across devices.</li>
<li>✅ Built with Astro.js: Leverages the powerful features of Astro.js for optimized performance and easy customize.</li>
<li>✅ Intuitive User Interface: Minimalist layout promotes ease of use, allowing users to find what they need without confusion.</li>
<li>✅ Speed Optimization: Engineered for exceptional loading times, ensuring that users dont have to wait on page loads.</li>
<li>✅ Customizable Components: Flexibility to customize elements easily, aligning with your brands identity and needs.</li>
<li>✅ SEO Friendly: Built with search engine optimization in mind, helping improve visibility and rankings.</li>
</ul>
<p>🔗🔗 <a href="https://luonmodels.vercel.app/" target="_blank">Live Demo</a></p>
</div>
{allImages.map((img, index) => (
<picture class="picture mt-8 md:mt-12 mb-8 md:mb-12 rounded-2xl overflow-hidden">
<Image src={img.default} alt={`Image ${index + 1}`} class="w-full h-full object-cover" loading="lazy" decoding="async" quality={100}/>
</picture>
))}
</div>
</section>
</main>
</div>
<ActionBar logo={logoUrl} tags={["Agency", "Website"]} url="https://luonmodels.vercel.app/" github="https://ricoui.gumroad.com/l/luon" visitLabel="Visit Site" />
</PostLayout>