diff --git a/next.config.mjs b/next.config.mjs index d921057..4753f8f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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 diff --git a/src/components/Container.tsx b/src/components/Container.tsx new file mode 100644 index 0000000..4ba9005 --- /dev/null +++ b/src/components/Container.tsx @@ -0,0 +1,5 @@ +import type { PropsWithChildren } from "react"; + +export const Container: React.FC = ({ children }) => { + return
{children}
; +}; diff --git a/src/components/Polaroid.tsx b/src/components/Polaroid.tsx new file mode 100644 index 0000000..3de6906 --- /dev/null +++ b/src/components/Polaroid.tsx @@ -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 = ({ title, imageSrc, rotation }) => { + const [showImage, setShowImage] = useState(false); + const ref = useRef(null); + const entry = useIntersectionObserver(ref, {}); + + const isOnScreen = !!entry?.isIntersecting; + + useEffect(() => { + if (isOnScreen) { + setShowImage(true); + } + }, [isOnScreen]); + + return ( +
+
+
+ {`Showcase +
+
+ + {title} + +
+
+ ); +}; diff --git a/src/components/ProjectDescription.tsx b/src/components/ProjectDescription.tsx new file mode 100644 index 0000000..35caed7 --- /dev/null +++ b/src/components/ProjectDescription.tsx @@ -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 = ({ + title, + color, + children, +}) => { + return ( +
+

{title}

+ +

{children}

+
+ ); +}; diff --git a/src/components/RootLayout.tsx b/src/components/RootLayout.tsx index 2dccb24..379f58c 100644 --- a/src/components/RootLayout.tsx +++ b/src/components/RootLayout.tsx @@ -10,6 +10,7 @@ const roadster = localFont({ preload: true, variable: "--font-roadster", }); + const jakarta = Plus_Jakarta_Sans({ subsets: ["latin"], weight: ["400", "600", "800"], diff --git a/src/constants/projects.ts b/src/constants/projects.ts new file mode 100644 index 0000000..eb9c230 --- /dev/null +++ b/src/constants/projects.ts @@ -0,0 +1,29 @@ +import type { Props as PolaroidProps } from "~/components/Polaroid"; +import type { Props as ProjectDescriptionProps } from "~/components/ProjectDescription"; + +export const projects: Array = [ + { + 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. ", + }, +]; diff --git a/src/hooks/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver.ts new file mode 100644 index 0000000..581ef15 --- /dev/null +++ b/src/hooks/useIntersectionObserver.ts @@ -0,0 +1,45 @@ +import { type RefObject, useEffect, useState } from "react"; + +interface Args extends IntersectionObserverInit { + freezeOnceVisible?: boolean; +} + +function useIntersectionObserver( + elementRef: RefObject, + { + threshold = 0, + root = null, + rootMargin = "0%", + freezeOnceVisible = false, + }: Args +): IntersectionObserverEntry | undefined { + const [entry, setEntry] = useState(); + + 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; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8c3023a..e6464db 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,10 @@ import { type NextPage } from "next"; import Head from "next/head"; -import Web from "../../public/icons/web.svg"; -import Mobile from "../../public/icons/mobile.svg"; -// import Design from "../../public/icons/design.svg"; - -interface BorderProps { - color: string; -} +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 ( @@ -19,7 +17,7 @@ const Home: NextPage = () => { <> - {/* */} + {/* */} {/* */} @@ -32,8 +30,8 @@ export default Home; const Hero = () => { return ( <> -

Sunrise

-

+

Sunrise

+

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{" "} @@ -42,20 +40,21 @@ const Hero = () => { ); }; + const Services = () => { const Services = [ { id: 1, icon: "web.svg", - borderColor: "border-[#F4A261]", + borderColor: "border-orange/20", title: "Websites", description: - "Fast. Responsive. Accessible. We specialize in building high-performing websites without leaving any users behind. ", + "Fast. Responsive. Accessible.\nWe specialize in building high-performing websites without leaving any users behind. ", }, { id: 2, icon: "mobile.svg", - borderColor: "border-[#2A9D8F]", + borderColor: "border-green/20", title: "Mobile Apps", description: "We build cross-platform mobile apps. Lorem ipsum dolor sit amet consectetur adipiscing elit.", @@ -63,7 +62,7 @@ const Services = () => { { id: 3, icon: "design.svg", - borderColor: "border-[#E76F51]", + borderColor: "border-red/20", title: "UI/UX Design", description: "First impressions matter. We know how to craft unique and impressive digital experiences.", @@ -71,10 +70,10 @@ const Services = () => { ]; return ( -

-
+
+
-

+

What we can do for you

@@ -84,19 +83,57 @@ const Services = () => { key={data.id} className="my-2 flex w-full flex-col items-center justify-start sm:w-1/3" > - service-icon -

{data.title}

+ service-icon +

{data.title}

-

- {data.description} -

+ {data.description.split("\n").map((line) => ( +

+ {line} +

+ ))}
))}
-
+
); }; -// const Projects = () => {} + +const Projects = () => { + return ( + +

+ Here are some of our past projects +

+
+ {projects.map((p) => ( +
+
+ +
+
+ + {p.children} + +
+
+ ))} +
+
+ ); +}; + // const About = () => {} // const Contact = () => {} diff --git a/tailwind.config.ts b/tailwind.config.ts index 2c5a016..874bda6 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,5 +1,11 @@ import { type Config } from "tailwindcss"; import { fontFamily } from "tailwindcss/defaultTheme"; +import plugin from "tailwindcss/plugin"; + +interface RecursiveKeyValuePair { + [key: string]: V | RecursiveKeyValuePair; +} +type CSSRuleObject = RecursiveKeyValuePair; 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;