이제 Carrot Market에 필요한 UI들을 만들기 위한 Tailwind코드를 본격적으로 짜보겠습니다.
Auth UI
pages/enter.tsx
import { useState } from "react";
type MethodType = "email" | "phone";
function cls(...classnames: string[]) {
return classnames.join(" ");
}
export default function Enter() {
const [method, setMethod] = useState<MethodType>("email");
const onEmailClick = () => setMethod("email");
const onPhoneClick = () => setMethod("phone");
return (
<div className="mt-16 px-4">
<h3 className="text-center text-3xl font-bold">Enter to Carrot</h3>
<div className="mt-8">
<div className="flex flex-col items-center">
<h5 className="text-sm font-medium text-gray-500">Enter using:</h5>
<div className="mt-8 grid w-full grid-cols-2 gap-16 border-b">
<button
className={cls(
"border-b-2 pb-4 font-medium",
method === "email"
? " border-orange-500 text-orange-500"
: "border-transparent text-gray-500"
)}
onClick={onEmailClick}
>
Email
</button>
<button
className={cls(
"border-b-2 pb-4 font-medium",
method === "phone"
? " border-orange-500 text-orange-500"
: "border-transparent text-gray-500"
)}
onClick={onPhoneClick}
>
Phone
</button>
</div>
</div>
<form className="mt-8 flex flex-col">
<label className="text-sm font-medium text-gray-700">
{method === "email" ? "Email address" : null}
{method === "phone" ? "Phone number" : null}
</label>
<div className="mt-2">
{method === "email" ? (
<input
type="email"
className="w-full appearance-none rounded-md border border-gray-300
px-3 py-2 placeholder-gray-400 shadow-sm focus:border-orange-500
focus:outline-none focus:ring-orange-500"
required
/>
) : null}
{method === "phone" ? (
<div className="flex rounded-md shadow-sm ">
<span className="flex select-none items-center justify-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
+82
</span>
<input
type="number"
className="w-full appearance-none rounded-r-md border border-gray-300
px-3 py-2 placeholder-gray-400 shadow-sm focus:border-orange-500 focus:outline-none focus:ring-orange-500"
required
/>
</div>
) : null}
</div>
<button
className="mt-6 rounded-md border border-transparent bg-orange-500
py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-orange-600
focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
>
{method === "email" ? "Get login link" : null}
{method === "phone" ? "Get one-time password" : null}
</button>
</form>
<div className="mt-8">
<div className="relative">
<div className="absolute w-full border-t border-gray-300" />
<div className="relative -top-3 text-center ">
<span className="bg-white px-2 text-sm text-gray-500">
Or enter with
</span>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<button className="flex justify-center rounded-md border border-gray-500 bg-white py-2 px-4 text-sm font-medium text-gray-500 shadow-sm hover:bg-gray-50">
<svg
className="h-5 w-5"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84" />
</svg>
</button>
<button className="flex justify-center rounded-md border border-gray-500 bg-white py-2 px-4 text-sm font-medium text-gray-500 shadow-sm hover:bg-gray-50">
<svg
className="h-5 w-5"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
);
}
디자인의 핵심은
위와 같이 tailwind/forms plugin을 설치해서 form을 꾸미는 것입니다. 이를 설치하고 나면 input의 형태가 조금 바뀌어 있을 겁니다. 이는 디자인 하기 더 쉬운 형태로 바뀐 것이라고 보면 됩니다. 여기 auth에서 form의 디자인 핵심은 input에서 focus되었을 때 나오는 파란색 outline을 없애주고, ring design을 넣는 것입니다.
또한 버튼도 ring design을 주어서 클릭 되었을 때, outline을 없애고, 주위로 윤곽선이 나오게끔 해주면 됩니다.
Home Screen UI
이제 물건들이 등장할 메인페이지를 만들어 봅시다. 다양한 물건들의 정보가 있는 페이지를 만들겁니다.
pages/home.tsx
import type { NextPage } from "next";
const Home: NextPage = () => {
return (
<div className="p flex flex-col space-y-5 py-10">
{[...Array(10)].map((_, i) => (
<div
key={i}
className="flex cursor-pointer justify-between border-b px-4 pb-4"
>
<div className="flex space-x-4">
<div className="h-20 w-20 rounded-md bg-gray-400" />
<div className="flex flex-col pt-2">
<h3 className="text-sm font-medium text-gray-900">New iPhone 14</h3>
<span className="text-xs text-gray-500">Black</span>
<span className="font-md mt-1 text-gray-900">$95</span>
</div>
</div>
<div className="flex items-end justify-end space-x-2">
<div className="flex items-center space-x-0.5 text-sm text-gray-600">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
<span>1</span>
</div>
<div className="flex items-center space-x-0.5 text-sm text-gray-600">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<span>1</span>
</div>
</div>
</div>
))}
<button className="fixed bottom-24 right-5 cursor-pointer rounded-full bg-orange-400 p-4 text-white shadow-xl transition-colors duration-300 hover:bg-orange-500">
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</button>
</div>
);
};
export default Home;
결과는 위와 같습니다. Main페이지에서의 디자인 팁은. flex, space-x-[number]를 통해서 적절한 layout을 화면에 배치하는 것에 있습니다. 또한 position:absolute로 부유 버튼을 만드는 것도 중요한 포인트입니다.
마지막으로 https://heroicons.com/ 사이트에서 svg아이콘을 tailwind에서 사용하는 방법도 중요합니다. 다양한 svg아이콘들이 있으며, font-awseom보다는 유용하지 않지만, 간단하게 확인할 때 좋습니다. 그 이유는 그냥 코드이기 때문에 용량이 적기 때문이죠. ( 위에 보이는 Home Screen의 하트와 댓글 모두 heroicons에서 복사해서 사용한 것 입니다.)
Item Detail UI
이제 물품들의 상세 정보를 나타내 줄 페이지를 만들어 보도록 하겠습니다.
pages/items/[id].tsx
import type { NextPage } from "next";
const ItemDetail: NextPage = () => {
return (
<div className="px-4 py-10">
<div className="mb-8">
<div className="h-96 bg-slate-300" />
<div className="space-x-300 mt-1 flex items-center border-t border-b">
<div className="h-12 w-12 rounded-full bg-slate-300" />
<div>
<p className="text-sm font-medium text-gray-700">Steve Jebs</p>
<p className="cursor-pointer text-xs font-medium text-gray-500">
View profile →
</p>
</div>
</div>
<div className="mt-5">
<h1 className="text-3xl font-bold text-gray-900">Galaxy S50</h1>
<span className="mt-3 block text-3xl text-gray-900">$140</span>
<p className="my-6 text-base text-gray-700">
My money's in that office, right? If she start giving me some
bullshit about it ain't there, and we got to go someplace else and
get it, I'm gonna shoot you in the head then and there. Then
I'm gonna shoot that bitch in the kneecaps, find out where my
goddamn money is. She gonna tell me too. Hey, look at me when I'm
talking to you, motherfucker. You listen: we go in there, and that ni**a
Winston or anybody else is in there, you the first motherfucker to get
shot. You understand?
</p>
<div className="flex items-center justify-between space-x-2">
<button
className="flex-1 rounded-md bg-orange-500 py-3 font-medium
text-white shadow-md hover:bg-orange-600 focus:outline-none focus:ring-2
focus:ring-orange-500 focus:ring-offset-2"
>
Talk to seller
</button>
<button className="flex items-center justify-center rounded-md p-3 text-gray-400 hover:bg-gray-100 hover:text-gray-500">
<svg
className="h-6 w-6 "
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</button>
</div>
</div>
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">Similar items</h2>
<div className="mt-6 grid grid-cols-2 gap-4">
{[1, 2, 3, 4, 5, 6].map((_, i) => (
<div key={i}>
<div className="mb-4 h-56 w-full bg-slate-300" />
<h3 className=" -mb-1 text-gray-700">Galaxy S60</h3>
<span className="text-sm font-medium text-gray-900">$6</span>
</div>
))}
</div>
</div>
</div>
);
};
export default ItemDetail;
Upload Item
이제 Home에서 + 버튼을 눌렀을 때 나올 수 있는 물건을 업로드 할 수 있는 UI를 만들어 보도록 하겠습니다.
pages/items/upload.tsx
import type { NextPage } from "next";
const Upload: NextPage = () => {
return (
<div className="px-4 py-16">
<div>
<div>
<label className="flex h-48 w-full cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-gray-300 py-6 text-gray-600 hover:border-orange-500 hover:text-orange-500">
<svg
className="h-12 w-12"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<input className="hidden" type="file" />
</label>
</div>
</div>
<div className="my-5">
<label htmlFor="price" className="mb-1 text-sm font-medium text-gray-700">
Price
</label>
<div className="relative flex items-center rounded-md shadow-sm">
<div className="pointer-events-none absolute left-0 flex items-center justify-center pl-3">
<span className="text-sm text-gray-500">$</span>
</div>
<input
id="price"
type="text"
placeholder="0.00"
className="w-full appearance-none rounded-md border border-gray-300
px-3 py-2 pl-7 placeholder-gray-400 shadow-sm
focus:border-orange-500 focus:outline-none focus:ring-orange-500"
/>
<div className="pointer-events-none absolute right-0 flex items-center pr-3">
<span className="text-gray-500">USD</span>
</div>
</div>
</div>
<div>
<label className="mb-1 text-sm font-medium text-gray-700">Description</label>
<textarea
rows={4}
className="focus:border-1 mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
/>
</div>
<button
className="mt-5 w-full rounded-md border border-transparent bg-orange-500
py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-orange-600
focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
>
Upload product
</button>
</div>
);
};
export default Upload;
여기서 중요한 점은 이제 점점 반복되는 디자인이 추가되고 있다는 점입니다. 이제 반복되는 스타일들을 컴포넌트로 분리해서 재사용 해야 합니다. 이는 좀 이따가 진행하도록 하겠습니다.
Community UI
이제 물건을 사고팔 떄 필요한 정보를 주고 받을 수 있는 화면을 만들어 보도록 하겠습니다.
pages/community/index.tsx
import type { NextPage } from "next";
const Community: NextPage = () => {
return (
<div className="space-y-8 px-4 py-16">
{[...Array(10)].map((_, i) => (
<div key={i} className="flex flex-col items-start">
<span className="flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800">
동네질문
</span>
<div className="mt-2 text-gray-700">
<span className="font-medium text-orange-500">Q.</span> What is the best
mandu restaurant?
</div>
<div className="mt-5 flex w-full items-center justify-between text-xs font-medium text-gray-500">
<span>현서</span>
<span>18시간 전</span>
</div>
<div className="mt-3 flex w-full space-x-5 border-t border-b-[2px] py-2.5 text-gray-700">
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>궁금해요 1</div>
</div>
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<div>답변 1</div>
</div>
</div>
</div>
))}
<button className="fixed bottom-24 right-5 cursor-pointer rounded-full bg-orange-400 p-4 text-white shadow-xl transition-colors duration-300 hover:bg-orange-500">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
></path>
</svg>
</button>
</div>
);
};
export default Community;
커뮤니티 화면도 재사용되는 작업이 많아지는데, 맨 마지막에 리팩토링 할 떄 다 정리해보도록 하겠습니다.
Community Detail UI
pages/community/[id].tsx
import type { NextPage } from "next";
const CommunityPostDetail: NextPage = () => {
return (
<div>
<span className="my-3 inline-flex items-center rounded-full bg-gray-100 px-4 py-0.5 text-xs font-medium text-gray-800">
동네질문
</span>
<div className="space-x-300 mb-3 flex items-center border-b py-2 px-3">
<div className="h-10 w-10 rounded-full bg-slate-300" />
<div>
<p className="text-sm font-medium text-gray-700">Steve Jebs</p>
<p className="cursor-pointer text-xs font-medium text-gray-500">
View profile →
</p>
</div>
</div>
<div>
<div className="mt-2 px-4 text-gray-700">
<span className="font-medium text-orange-500">Q.</span> What is the best
mandu restaurant?
</div>
<div className="mt-3 flex w-full space-x-5 border-t border-b-[2px] py-2.5 px-4 text-gray-700">
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>궁금해요 1</div>
</div>
<div className="flex items-center space-x-2 text-sm">
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<div>답변 1</div>
</div>
</div>
</div>
<div className="my-5 space-y-5 px-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-start space-x-3">
<div className="h-8 w-8 rounded-full bg-slate-200" />
<div className="">
<span className="block text-sm font-medium text-gray-700">
Steve Jebs
</span>
<span className="block text-xs font-medium text-gray-500">
2시간 전
</span>
<p className="mt-[1.5px] text-gray-700">
The best mandu restaurant is the one next to my house.
</p>
</div>
</div>
))}
</div>
<div className="px-4">
<textarea
rows={4}
placeholder="Answer this question!"
className="focus:border-1 mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
/>
<button
className="mt-2 w-full rounded-md border border-transparent bg-orange-500
py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-orange-600
focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
>
Reply
</button>
</div>
</div>
);
};
export default CommunityPostDetail;
Write UI
Write 페이지는 너무 간단해서 설명 없이 코드와 결과만 보여드리도록 하겠습니다.
pages/community/write.tsx
import type { NextPage } from "next";
const Write: NextPage = () => {
return (
<div className="px-4 py-10">
<div className="px-4">
<textarea
rows={4}
placeholder="Ask a question!"
className="focus:border-1 mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
/>
<button
className="mt-2 w-full rounded-md border border-transparent bg-orange-500
py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-orange-600
focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
>
Reply
</button>
</div>
</div>
);
};
export default Write;
'Web > CloneCoding' 카테고리의 다른 글
[Carrot Market] #5 TAILWIND CLONING UI - 3 (0) | 2022.05.01 |
---|---|
[Carrot Market] #5 TAILWIND CLONING UI - 2 (0) | 2022.05.01 |
[Carrot-Market] #4 TAILWIND - 2 (0) | 2022.04.30 |
[Carrot-Market] #4 TAILWIND - 1 (0) | 2022.04.30 |
[Carrot-Market] #3 SETUP (0) | 2022.04.29 |