Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coding Task-Rui Song #1

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
API_KEY=
BASE_URL=https://caruuto.27.works/api/v1
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# 27W Coding Task
<div>
This is a Next.js application with two pages: an article page (/posts/[an-article-title]) and a member's profile page (/profile). Its functionalities include data fetching and rendering, CSS layouts with Tailwind, form validation and form state capturing.
</div>
</div>

## <div>How to Run</div>

**Cloning the Repository**

```bash
git clone https://github.com/RuiSong1998/coding-task-rui-song.git
cd coding-task-rui-song
```

**Installation**

```bash
npm install
```

**Set Up Environment Variables**

Create a new file named `.env` in the root of the project and add the API key.

**Running the Project**

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in the browser to view the project.
17 changes: 17 additions & 0 deletions actions/dealers-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const getDealers = async () => {
const res = await fetch(`${process.env.BASE_URL}/dealers`, {
method: "GET",
headers: {
Authorization: process.env.API_KEY,
"Content-Type": "application/json",
},
});

if (!res.ok) {
throw new Error("Failed to get your dealers");
}

const data = await res.json();

return data;
};
27 changes: 27 additions & 0 deletions actions/post-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { notFound } from "next/navigation";

export const getPost = async (slug) => {
const res = await fetch(
`${process.env.BASE_URL}/posts?filter={"slug": "${slug}"}`,
{
method: "GET",
headers: {
Authorization: process.env.API_KEY,
"Content-Type": "application/json",
},
}
);

if (!res.ok) {
notFound();
}

const data = await res.json();
const article = data?.results?.[0];

if (!article) {
notFound();
}

return article;
};
84 changes: 84 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
@font-face {
font-family: "Futura PT Bold";
src: url("../public/fonts/FuturaPTBold.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Book";
src: url("../public/fonts/FuturaPTBook.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Demi";
src: url("../public/fonts/FuturaPTDemi.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Extra Bold";
src: url("../public/fonts/FuturaPTExtraBold.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Heavy";
src: url("../public/fonts/FuturaPTHeavy.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Light";
src: url("../public/fonts/FuturaPTLight.otf") format("opentype");
}

@font-face {
font-family: "Futura PT Medium";
src: url("../public/fonts/FuturaPTMedium.otf") format("opentype");
}
}

.button {
@apply flex items-center justify-between px-4 py-2 gap-x-2.5 rounded-md transition-all ease-in-out duration-300 text-[#8C8C8C] border border-[#8C8C8C] hover:opacity-50;
}

.button-colored {
@apply text-black border border-transparent bg-gradient-to-r from-[#F2CB13] to-[#FF9900] hover:opacity-70;
}

.container {
@apply mx-auto py-4 px-4 md:px-10 lg:px-12;
}

h3 {
@apply font-futura_pt_medium text-xl;
}

strong {
@apply text-3xl font-futura_pt_extrabold;
}

p {
@apply text-[#B1B3B3] font-futura_pt_book text-lg;
}

p strong {
@apply font-futura_pt_bold text-white text-lg;
}

p img {
@apply w-full h-auto py-4 rounded-lg;
}

.article a {
@apply text-white font-futura_pt_medium underline;
}

.article tbody tr:first-child {
@apply hidden;
}

.article tr {
@apply font-futura_pt_light text-[#B1B3B3] border-b border-[#8C8C8C] border-opacity-20;
}
14 changes: 14 additions & 0 deletions app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "./globals.css";

export const metadata = {
title: "Radical",
description: "Radical Motorsport",
};

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
132 changes: 132 additions & 0 deletions app/posts/[article-title]/components/article.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Link from "next/link";
import Image from "next/image";
import ArticleAuthor from "@/public/images/article_author.jpg";

export const Article = ({
createdAt,
author,
title,
subtitle,
bodyText,
imageGallery,
}) => {
const getDaySuffix = (day) => {
if (day >= 11 && day <= 13) {
return "th";
}
switch (day % 10) {
case 1:
return "st";
case 2:
return "nd";
case 3:
return "rd";
default:
return "th";
}
};
const date = new Date(createdAt);

const day = date.getDate();
const month = date.toLocaleString("en-US", { month: "long" });
const year = date.getFullYear();
const suffix = getDaySuffix(day);

const formattedDate = `${day}${suffix} ${month}, ${year}`;

const createSubtitle = () => ({ __html: subtitle });

const createBodyText = () => ({ __html: bodyText });

return (
<article className="container grid grid-cols-12 items-center min-h-screen -mt-12 md:-mt-48 relative mb-14">
<div className="col-span-12 col-start-1 md:col-span-10 lg:col-span-8 lg:col-start-2 xl:col-span-7 xl:col-start-3 xl:pr-10 flex flex-col gap-y-7">
<div>
<div className="flex items-center mb-3 space-x-1 text-sm text-[#F2CB13] font-futura_pt_light">
<Link
className="opacity-70 hover:opacity-100 underline underline-offset-2"
href="/"
>
Home
</Link>
<span>/</span>
<Link
className="opacity-70 hover:opacity-100 underline underline-offset-2"
href="/news"
>
Latest News
</Link>
</div>
<h1 className="text-6xl font-futura_pt_bold mb-2 bg-clip-text bg-gradient-to-br from-[#F2CB13] to-[#FF9900] text-transparent uppercase">
{title}
</h1>
<svg
className="w-4 h-4 my-5 stroke-[#B1B3B3] hover:stroke-[#F2CB13] stroke-1.5 stroke-linecap-round stroke-linejoin-round transition-all ease-in-out duration-300 cursor-pointer"
viewBox="0 0 10 13"
>
<path d="M1.0625 2.5625C1.0625 1.73407 1.73407 1.0625 2.5625 1.0625H7.4375C8.26593 1.0625 8.9375 1.73407 8.9375 2.5625V11.9375L5 8.5625L1.0625 11.9375V2.5625Z" />
</svg>
<div
className="text-white text-xl font-futura_pt_medium"
dangerouslySetInnerHTML={createSubtitle()}
></div>
</div>
<div className="flex justify-between items-center">
<div className="flex items-center w-full gap-3 text-white font-futura_pt_light">
<Image
src={ArticleAuthor}
className="object-cover w-10 h-10 rounded-full"
alt="article author"
/>
<div className="flex font-futura_pt_book">
<span>{`${author?.firstName || "John"} ${
author?.lastName || "Snow"
}`}</span>
<div className="w-1 h-1 m-3 rounded-full bg-[#D9D9D9] bg-opacity-30"></div>
<span>{formattedDate}</span>
</div>
</div>
<div className="flex items-center h-5 space-x-3 text-[#B1B3B3]">
<span className="border border-[#B1B3B3] border-opacity-50 border-1 hover:border-[#F2CB13] transition-all ease-in-out duration-300 rounded-full cursor-pointer h-9 w-9 flex justify-center items-center">
<svg
className="w-4 h-4 stroke-[#B1B3B3] hover:stroke-[#F2CB13] stroke-1.5 stroke-linecap-round stroke-linejoin-round transition-all ease-in-out duration-300"
viewBox="0 0 10 13"
>
<path d="M1.0625 2.5625C1.0625 1.73407 1.73407 1.0625 2.5625 1.0625H7.4375C8.26593 1.0625 8.9375 1.73407 8.9375 2.5625V11.9375L5 8.5625L1.0625 11.9375V2.5625Z" />
</svg>
</span>
<span className="border border-[#B1B3B3] border-opacity-50 border-1 hover:border-[#F2CB13] hover:text-[#F2CB13] transition-all ease-in-out duration-300 rounded-full cursor-pointer h-9 w-9 flex justify-center items-center">
<svg
className="w-6 h-6 stroke-[#B1B3B3] hover:stroke-[#F2CB13] stroke-1.5 stroke-linecap-round stroke-linejoin-round transition-all ease-in-out duration-300"
viewBox="0 0 18 19"
>
<path d="M14.4375 5.75C14.4375 6.68198 13.682 7.4375 12.75 7.4375C11.818 7.4375 11.0625 6.68198 11.0625 5.75C11.0625 4.81802 11.818 4.0625 12.75 4.0625C13.682 4.0625 14.4375 4.81802 14.4375 5.75Z" />
<path d="M6.9375 9.5C6.9375 10.432 6.18198 11.1875 5.25 11.1875C4.31802 11.1875 3.5625 10.432 3.5625 9.5C3.5625 8.56802 4.31802 7.8125 5.25 7.8125C6.18198 7.8125 6.9375 8.56802 6.9375 9.5Z" />
<path d="M14.4375 13.25C14.4375 14.182 13.682 14.9375 12.75 14.9375C11.818 14.9375 11.0625 14.182 11.0625 13.25C11.0625 12.318 11.818 11.5625 12.75 11.5625C13.682 11.5625 14.4375 12.318 14.4375 13.25Z" />
<path d="M10.875 12.5L6.75 10.625" />
<path d="M10.875 6.875L6.75 8.75" />
</svg>
</span>
</div>
</div>
<div
dangerouslySetInnerHTML={createBodyText()}
className="flex flex-col gap-y-6 article"
></div>
<div className="w-full relative flex flex-col gap-y-7">
{imageGallery?.map((image) => (
<div key={image?.id}>
<Image
src={image?.url}
className="object-cover rounded-lg"
width={800}
height={500}
alt="article image gallery"
/>
</div>
))}
</div>
</div>
</article>
);
};
16 changes: 16 additions & 0 deletions app/posts/[article-title]/components/hero-section.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Image from "next/image";

export const HeroSection = ({ heroImage }) => (
<div className="relative w-full aspect-w-4 aspect-h-3 lg:aspect-w-16 lg:aspect-h-9">
<div className="w-full min-h-[1000px] opacity-80">
<div className="absolute w-full bg-gradient-to-b from-black z-10 h-[50%]"></div>
<Image
className="object-cover"
fill
src={heroImage}
alt="Article image"
/>
<div className="absolute w-full h-[50%] bottom-0 bg-gradient-to-t from-black"></div>
</div>
</div>
);
13 changes: 13 additions & 0 deletions app/posts/[article-title]/components/profile-button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Link from "next/link";

export const ProfileButton = () => (
<Link className="button button-colored" href="/profile">
<span className="font-futura_pt_bold text-sm">MY RADICAL</span>
<svg
className="w-2 h-3 fill-none stroke-current stroke-2"
viewBox="0 0 7 10"
>
<path d="M1.4646 1.49451L5.00013 5.03004L1.4646 8.56557" />
</svg>
</Link>
);
Loading