Compare commits
14 Commits
SR-010-Cre
...
SR-002-Ser
| Author | SHA1 | Date | |
|---|---|---|---|
| b95515d7c0 | |||
|
|
1ba4ddf869 | ||
| d18a6ca60b | |||
|
|
c7cae41abf | ||
| f809abcf6d | |||
|
|
eb44659f7d | ||
| 317081841c | |||
| 8f738b0ad2 | |||
| da5159aa48 | |||
|
|
f09f6bfdf1 | ||
|
|
5f1cdfda5b | ||
|
|
0aacc551d6 | ||
|
|
adeedd3570 | ||
|
|
9804acf49b |
@@ -7,6 +7,16 @@ await import("./src/env.mjs");
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "picsum.photos",
|
||||
port: "",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/**
|
||||
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
|
||||
|
||||
5
src/components/Container.tsx
Normal file
5
src/components/Container.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
export const Container: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
return <div className="container">{children}</div>;
|
||||
};
|
||||
48
src/components/Polaroid.tsx
Normal file
48
src/components/Polaroid.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useIntersectionObserver from "~/hooks/useIntersectionObserver";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
rotation: string;
|
||||
}
|
||||
|
||||
export const Polaroid: React.FC<Props> = ({ title, imageSrc, rotation }) => {
|
||||
const [showImage, setShowImage] = useState(false);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const entry = useIntersectionObserver(ref, {});
|
||||
|
||||
const isOnScreen = !!entry?.isIntersecting;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOnScreen) {
|
||||
setShowImage(true);
|
||||
}
|
||||
}, [isOnScreen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`m-5 w-52 rounded bg-[#F8F2EA] text-black shadow-md sm:w-64 lg:w-80 ${rotation}`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col items-center justify-between gap-y-3 p-4 sm:gap-y-4 sm:p-5 lg:gap-y-5 lg:p-6">
|
||||
<div className="relative aspect-square w-full">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={`Showcase of ${title}`}
|
||||
fill
|
||||
className="object-cover shadow-inner"
|
||||
/>
|
||||
<div
|
||||
className={`absolute inset-0 transition-colors duration-1000 ${
|
||||
showImage ? "bg-transparent" : "bg-[#3D3D3D]"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<span ref={ref} className="font-semibold text-r-lg">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
24
src/components/ProjectDescription.tsx
Normal file
24
src/components/ProjectDescription.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as Separator from "@radix-ui/react-separator";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
color: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ProjectDescription: React.FC<Props> = ({
|
||||
title,
|
||||
color,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="max-w-4xl text-black">
|
||||
<h3 className="font-semibold text-r-xl">{title}</h3>
|
||||
<Separator.Root
|
||||
className={`${color} my-2 h-1 w-full rounded-full`}
|
||||
decorative
|
||||
/>
|
||||
<p className="leading-relaxed text-r-base">{children}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ const roadster = localFont({
|
||||
preload: true,
|
||||
variable: "--font-roadster",
|
||||
});
|
||||
|
||||
const jakarta = Plus_Jakarta_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "600", "800"],
|
||||
|
||||
29
src/constants/projects.ts
Normal file
29
src/constants/projects.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Props as PolaroidProps } from "~/components/Polaroid";
|
||||
import type { Props as ProjectDescriptionProps } from "~/components/ProjectDescription";
|
||||
|
||||
export const projects: Array<PolaroidProps & ProjectDescriptionProps> = [
|
||||
{
|
||||
title: "Parallel",
|
||||
color: "bg-orange",
|
||||
imageSrc: "https://picsum.photos/seed/Parallel/200",
|
||||
rotation: "-rotate-12",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
},
|
||||
{
|
||||
title: "DropNote",
|
||||
color: "bg-yellow",
|
||||
imageSrc: "https://picsum.photos/seed/DropNote/200",
|
||||
rotation: "rotate-6",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
},
|
||||
{
|
||||
title: "Flurry Waitlist",
|
||||
color: "bg-green",
|
||||
imageSrc: "https://picsum.photos/seed/Flurry/200",
|
||||
rotation: "-rotate-3",
|
||||
children:
|
||||
"We started Parallel to help connect educators with content creators to make it easier to create high-quality educational content for platforms like YouTube. We built the platform with high performance, scalability, and accessibility in mind. That's why we chose powerful tools like Next.js, TailwindCSS, RadixUI, Prisma, PlanetScale, and Vercel's hosting platform. ",
|
||||
},
|
||||
];
|
||||
45
src/hooks/useIntersectionObserver.ts
Normal file
45
src/hooks/useIntersectionObserver.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { type RefObject, useEffect, useState } from "react";
|
||||
|
||||
interface Args extends IntersectionObserverInit {
|
||||
freezeOnceVisible?: boolean;
|
||||
}
|
||||
|
||||
function useIntersectionObserver(
|
||||
elementRef: RefObject<Element>,
|
||||
{
|
||||
threshold = 0,
|
||||
root = null,
|
||||
rootMargin = "0%",
|
||||
freezeOnceVisible = false,
|
||||
}: Args
|
||||
): IntersectionObserverEntry | undefined {
|
||||
const [entry, setEntry] = useState<IntersectionObserverEntry>();
|
||||
|
||||
const frozen = entry?.isIntersecting && freezeOnceVisible;
|
||||
|
||||
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
|
||||
setEntry(entry);
|
||||
};
|
||||
|
||||
const stringifiedThreshold = JSON.stringify(threshold);
|
||||
|
||||
useEffect(() => {
|
||||
const node = elementRef?.current; // DOM Ref
|
||||
const hasIOSupport = !!window.IntersectionObserver;
|
||||
|
||||
if (!hasIOSupport || frozen || !node) return;
|
||||
|
||||
const observerParams = { threshold, root, rootMargin };
|
||||
const observer = new IntersectionObserver(updateEntry, observerParams);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return () => observer.disconnect();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [elementRef?.current, stringifiedThreshold, root, rootMargin, frozen]);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export default useIntersectionObserver;
|
||||
@@ -1,5 +1,10 @@
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { Container } from "~/components/Container";
|
||||
import { Polaroid } from "~/components/Polaroid";
|
||||
import { ProjectDescription } from "~/components/ProjectDescription";
|
||||
import { projects } from "~/constants/projects";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
@@ -11,8 +16,8 @@ const Home: NextPage = () => {
|
||||
</Head>
|
||||
<>
|
||||
<Hero />
|
||||
{/* <Services /> */}
|
||||
{/* <Projects /> */}
|
||||
<Services />
|
||||
<Projects />
|
||||
{/* <About /> */}
|
||||
{/* <Contact /> */}
|
||||
</>
|
||||
@@ -25,8 +30,8 @@ export default Home;
|
||||
const Hero = () => {
|
||||
return (
|
||||
<>
|
||||
<h1 className="py-3 font-roadster text-5xl text-black">Sunrise</h1>
|
||||
<p className="leading-relaxed text-black">
|
||||
<h1 className="py-3 font-roadster text-black text-r-3xl">Sunrise</h1>
|
||||
<p className="leading-relaxed text-black text-r-xl">
|
||||
Creating software that looks and works great is our specialty at
|
||||
Sunrise. Our team of experts combines artistry and technical know-how to
|
||||
craft solutions that will make your business{" "}
|
||||
@@ -35,7 +40,100 @@ const Hero = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
// const Services = () => {}
|
||||
// const Projects = () => {}
|
||||
|
||||
const Services = () => {
|
||||
const Services = [
|
||||
{
|
||||
id: 1,
|
||||
icon: "web.svg",
|
||||
borderColor: "border-orange/20",
|
||||
title: "Websites",
|
||||
description:
|
||||
"Fast. Responsive. Accessible.\nWe specialize in building high-performing websites without leaving any users behind. ",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: "mobile.svg",
|
||||
borderColor: "border-green/20",
|
||||
title: "Mobile Apps",
|
||||
description:
|
||||
"We build cross-platform mobile apps. Lorem ipsum dolor sit amet consectetur adipiscing elit.",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: "design.svg",
|
||||
borderColor: "border-red/20",
|
||||
title: "UI/UX Design",
|
||||
description:
|
||||
"First impressions matter. We know how to craft unique and impressive digital experiences.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="my-10 bg-sand">
|
||||
<Container>
|
||||
<div>
|
||||
<h2 className="text-center font-extrabold text-r-2xl sm:text-left">
|
||||
What we can do for you
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-col md:flex-row">
|
||||
{Services.map((data) => (
|
||||
<div
|
||||
key={data.id}
|
||||
className="my-2 flex w-full flex-col items-center justify-start sm:w-1/3"
|
||||
>
|
||||
<Image
|
||||
src={`icons/${data.icon}`}
|
||||
alt="service-icon"
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
<h3 className="font-semibold text-r-xl">{data.title}</h3>
|
||||
<div className={`my-5 w-3/4 border-2 ${data.borderColor}`} />
|
||||
{data.description.split("\n").map((line) => (
|
||||
<p key={line} className="w-3/4 text-center text-r-base">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects = () => {
|
||||
return (
|
||||
<Container>
|
||||
<h2 className="my-10 text-center font-extrabold text-black text-r-2xl md:mt-16">
|
||||
Here are some of our past projects
|
||||
</h2>
|
||||
<div className="pb-20 [&>*:nth-child(odd)]:md:flex-row">
|
||||
{projects.map((p) => (
|
||||
<div
|
||||
key={p.title}
|
||||
className="mb-10 flex flex-col items-center gap-x-12 gap-y-6 md:flex-row-reverse lg:my-0"
|
||||
>
|
||||
<div>
|
||||
<Polaroid
|
||||
title={p.title}
|
||||
imageSrc={p.imageSrc}
|
||||
rotation={p.rotation}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectDescription title={p.title} color={p.color}>
|
||||
{p.children}
|
||||
</ProjectDescription>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
// const About = () => {}
|
||||
// const Contact = () => {}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { type Config } from "tailwindcss";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
interface RecursiveKeyValuePair<K extends keyof any = string, V = string> {
|
||||
[key: string]: V | RecursiveKeyValuePair<K, V>;
|
||||
}
|
||||
type CSSRuleObject = RecursiveKeyValuePair<string, null | string | string[]>;
|
||||
|
||||
export default {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
@@ -9,6 +15,15 @@ export default {
|
||||
center: true,
|
||||
padding: "1.5rem",
|
||||
},
|
||||
fontSize: {
|
||||
xs: ["0.875rem", { lineHeight: "1rem" }], // 14px 0.875rem
|
||||
sm: ["1rem", { lineHeight: "1.25rem" }], // 16px 1rem
|
||||
base: ["1.25rem", { lineHeight: "1.5rem" }], // 20px 1.25rem
|
||||
lg: ["1.5rem", { lineHeight: "1.75rem" }], // 24px 1.5rem
|
||||
xl: ["2rem", { lineHeight: "1.75rem" }], // 32px 2rem
|
||||
"2xl": ["2.5rem", { lineHeight: "2rem" }], // 40px 2.5rem
|
||||
"3xl": ["3rem", { lineHeight: "2.25rem" }], // 48px 3rem
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-jakarta)"],
|
||||
roadster: ["var(--font-roadster)", ...fontFamily.sans],
|
||||
@@ -21,9 +36,46 @@ export default {
|
||||
orange: "#F4A261",
|
||||
red: "#E76F51",
|
||||
white: "#EAD9C2",
|
||||
sand: "#F0D4B2",
|
||||
black: "#2E251E",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
// Responsive Typography
|
||||
plugin(function ({ addUtilities, theme }) {
|
||||
const fontSizes = Object.keys(theme("fontSize"));
|
||||
|
||||
const buildCSS = (fontSize: string): CSSRuleObject => {
|
||||
const fsIndex = fontSizes.indexOf(fontSize);
|
||||
|
||||
if (fsIndex > 1) {
|
||||
const desktop = fontSize;
|
||||
const tablet = fontSizes[fsIndex - 1] ?? fontSize;
|
||||
const mobile = fontSizes[fsIndex - 2] ?? fontSize;
|
||||
|
||||
return {
|
||||
"font-size": theme(`fontSize.${mobile}`),
|
||||
"@screen md": {
|
||||
"font-size": theme(`fontSize.${tablet}`),
|
||||
},
|
||||
"@screen 2xl": {
|
||||
"font-size": theme(`fontSize.${desktop}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { "font-size": theme(`fontSize.${fontSize}`) };
|
||||
};
|
||||
|
||||
addUtilities({
|
||||
".text-r-3xl": buildCSS("3xl"),
|
||||
".text-r-2xl": buildCSS("2xl"),
|
||||
".text-r-xl": buildCSS("xl"),
|
||||
".text-r-lg": buildCSS("lg"),
|
||||
".text-r-base": buildCSS("base"),
|
||||
".text-r-sm": buildCSS("sm"),
|
||||
});
|
||||
}),
|
||||
],
|
||||
} satisfies Config;
|
||||
|
||||
Reference in New Issue
Block a user