Skip to content

Commit 5495c3c

Browse files
committed
feat: init friends page
1 parent 761b17d commit 5495c3c

File tree

7 files changed

+275
-1
lines changed

7 files changed

+275
-1
lines changed

next.config.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@ import NextBundleAnalyzer from '@next/bundle-analyzer';
33

44
let nextConfig = {
55
images: {
6-
domains: ['innei.in','images.unsplash.com','assets.aceternity.com'],
6+
domains: ['innei.in', 'images.unsplash.com', 'assets.aceternity.com'],
7+
remotePatterns: [
8+
{
9+
protocol: 'https',
10+
hostname: '**', // 允许加载所有远程 HTTPS 主机的图片
11+
},
12+
{
13+
protocol: 'http',
14+
hostname: '**', // 允许加载所有远程 HTTP 主机的图片
15+
},
16+
],
717
},
818
};
919

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@iconify-json/material-symbols": "^1.2.4",
2222
"@iconify-json/mingcute": "^1.2.1",
2323
"@next/bundle-analyzer": "^14.2.15",
24+
"@tailwindcss/typography": "^0.5.15",
2425
"@types/chroma-js": "^2.4.4",
2526
"@types/pngjs": "^6.0.5",
2627
"@types/prettier": "^3.0.0",

pnpm-lock.yaml

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(app)/friends/layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Metadata } from 'next';
2+
import type { PropsWithChildren } from 'react';
3+
4+
import { NormalContainer } from '@/components/layout/container/Normal';
5+
6+
export const metadata: Metadata = {
7+
title: '朋友们',
8+
};
9+
10+
export default async function (props: PropsWithChildren) {
11+
return <NormalContainer>{props.children}</NormalContainer>;
12+
}

src/app/(app)/friends/page.tsx

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
'use client';
2+
3+
import { motion } from 'framer-motion';
4+
import { memo, useState } from 'react';
5+
import Image from 'next/image';
6+
7+
interface FriendModel {
8+
name: string;
9+
url: string;
10+
avatar: string;
11+
desc: string;
12+
}
13+
14+
export default function Friends() {
15+
const friendData: FriendModel[] = [
16+
{
17+
name: 'Alice',
18+
url: 'https://www.baidu.com',
19+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
20+
desc: 'A passionate developer and a great friend.',
21+
},
22+
{
23+
name: 'Bob',
24+
url: 'https://bob.example.com',
25+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
26+
desc: 'Bob loves working on open-source projects.',
27+
},
28+
{
29+
name: 'Charlie',
30+
url: 'https://charlie.example.com',
31+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
32+
desc: 'Charlie is a fantastic designer and loves to create beautiful UIs.',
33+
},
34+
{
35+
name: 'Dave',
36+
url: 'https://dave.example.com',
37+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
38+
desc: 'A data science enthusiast and machine learning expert.',
39+
},
40+
{
41+
name: 'Eve',
42+
url: 'https://eve.example.com',
43+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
44+
desc: 'Eve loves cybersecurity and cryptography.',
45+
},
46+
{
47+
name: 'Frank',
48+
url: 'https://frank.example.com',
49+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
50+
desc: 'Frank is a backend engineer with years of experience.',
51+
},
52+
{
53+
name: 'Grace',
54+
url: 'https://grace.example.com',
55+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
56+
desc: 'Grace is a machine learning researcher.',
57+
},
58+
{
59+
name: 'Heidi',
60+
url: 'https://heidi.example.com',
61+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
62+
desc: 'Heidi enjoys working on cloud computing.',
63+
},
64+
{
65+
name: 'Ivan',
66+
url: 'https://ivan.example.com',
67+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
68+
desc: 'Ivan specializes in database optimization.',
69+
},
70+
{
71+
name: 'Judy',
72+
url: 'https://judy.example.com',
73+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
74+
desc: 'Judy is a frontend developer with a passion for animations.',
75+
},
76+
{
77+
name: 'Karl',
78+
url: 'https://karl.example.com',
79+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
80+
desc: 'Karl is a mobile developer working on iOS apps.',
81+
},
82+
{
83+
name: 'Lara',
84+
url: 'https://lara.example.com',
85+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
86+
desc: 'Lara enjoys writing clean, maintainable code.',
87+
},
88+
{
89+
name: 'Mallory',
90+
url: 'https://mallory.example.com',
91+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
92+
desc: 'Mallory is an expert in penetration testing.',
93+
},
94+
{
95+
name: 'Nina',
96+
url: 'https://nina.example.com',
97+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
98+
desc: 'Nina is passionate about AR/VR development.',
99+
},
100+
{
101+
name: 'Oscar',
102+
url: 'https://oscar.example.com',
103+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
104+
desc: 'Oscar loves contributing to open-source projects.',
105+
},
106+
{
107+
name: 'Peggy',
108+
url: 'https://peggy.example.com',
109+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
110+
desc: 'Peggy is a blockchain developer.',
111+
},
112+
{
113+
name: 'Quinn',
114+
url: 'https://quinn.example.com',
115+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
116+
desc: 'Quinn focuses on DevOps and cloud infrastructure.',
117+
},
118+
{
119+
name: 'Rita',
120+
url: 'https://rita.example.com',
121+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
122+
desc: 'Rita is an AI researcher and educator.',
123+
},
124+
{
125+
name: 'Steve',
126+
url: 'https://steve.example.com',
127+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
128+
desc: 'Steve enjoys developing Android applications.',
129+
},
130+
{
131+
name: 'Trudy',
132+
url: 'https://trudy.example.com',
133+
avatar: 'https://avatars.githubusercontent.com/u/116412388?v=4',
134+
desc: 'Trudy works on full-stack web applications.',
135+
},
136+
];
137+
138+
return (
139+
<div>
140+
<header className="prose prose-p:my-2 font-mono">
141+
<h1>朋友们</h1>
142+
<h3>海内存知己,天涯若比邻</h3>
143+
</header>
144+
145+
<main className="mt-10 flex w-full flex-col">
146+
<FriendCardList data={friendData} />
147+
</main>
148+
</div>
149+
);
150+
}
151+
152+
const FriendCardList = ({ data }: { data: FriendModel[] }) => (
153+
<div className="grid grid-cols-2 gap-6 md:grid-cols-3">
154+
{data.map((friendModel) => {
155+
return <FriendCard friendModel={friendModel} />;
156+
})}
157+
</div>
158+
);
159+
const FriendCard = ({ friendModel }: { friendModel: FriendModel }) => {
160+
const [enter, setEnter] = useState(false);
161+
162+
return (
163+
<motion.div
164+
key={friendModel.name}
165+
role="link"
166+
aria-label={`Go to ${friendModel.name}'s website`}
167+
className="relative flex flex-col items-center justify-center cursor-pointer"
168+
onMouseEnter={() => setEnter(true)}
169+
onMouseLeave={() => setEnter(false)}
170+
onClick={() => window.open(friendModel.url, '_blank')}
171+
rel="noreferrer"
172+
>
173+
{enter && <LayoutBg />}
174+
<Image
175+
src={friendModel.avatar}
176+
height={64}
177+
width={64}
178+
alt={`Avatar of ${friendModel.name}`}
179+
className=" rounded-md"
180+
/>
181+
<span className="flex h-full flex-col items-center justify-center space-y-2 py-3">
182+
<span className="text-lg font-medium">{friendModel.name}</span>
183+
<span className="line-clamp-2 text-balance break-all text-center text-sm text-base-content/80">
184+
{friendModel.desc}
185+
</span>
186+
</span>
187+
</motion.div>
188+
);
189+
};
190+
const LayoutBg = memo(() => {
191+
return (
192+
<motion.span
193+
layoutId="bg"
194+
className="absolute -inset-2 z-[-1] rounded-md bg-slate-200/80 dark:bg-neutral-600/80 pointer-events-none"
195+
initial={{ opacity: 0.8, scale: 0.8 }}
196+
animate={{ opacity: 1, scale: 1 }}
197+
exit={{ opacity: 0, scale: 0.8, transition: { delay: 0.2 } }}
198+
/>
199+
);
200+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { PropsWithChildren } from 'react';
2+
3+
import { cn } from '@/lib/helper';
4+
5+
export const NormalContainer = (props: PropsWithChildren & { className?: string }) => {
6+
const { children, className } = props;
7+
8+
return (
9+
<div
10+
className={cn(
11+
'mx-auto mt-14 max-w-3xl px-2 lg:mt-[80px] lg:px-0 2xl:max-w-4xl',
12+
'[&_header.prose]:mb-[80px]',
13+
className,
14+
)}
15+
>
16+
{children}
17+
</div>
18+
);
19+
};

tailwind.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import './plugins/tw-css-plugin';
33

44
import daisyui from 'daisyui';
5+
import typography from '@tailwindcss/typography';
56
import type { Config } from 'tailwindcss';
67
import { withTV } from 'tailwind-variants/transformer';
78
import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons';
@@ -265,6 +266,7 @@ const config: Config = {
265266
},
266267
}),
267268
daisyui,
269+
typography,
268270
require('./src/styles/theme.css'),
269271
require('./src/styles/layer.css'),
270272
require('./src/styles/animation.css'),

0 commit comments

Comments
 (0)