├── .eslintrc
├── .gitignore
├── README.md
├── components
├── Button.js
├── FeaturedProjectCard.js
├── Icon.js
├── MockupToolbar.js
├── NewIcon.js
├── ProjectCard.js
├── SourceButton.js
├── blog
│ ├── BlogItem.js
│ └── BlogList.js
├── icons
│ ├── AdobeXd.js
│ ├── AfterEffects.js
│ ├── Bootstrap.js
│ ├── Css.js
│ ├── Express.js
│ ├── ExternalLink.js
│ ├── Figma.js
│ ├── Firebase.js
│ ├── GitHub.js
│ ├── GitHubProfile.js
│ ├── Html.js
│ ├── Illustrator.js
│ ├── Javascript.js
│ ├── LinkedInProfile.js
│ ├── MongoDb.js
│ ├── NextJs.js
│ ├── NodeJs.js
│ ├── Photoshop.js
│ ├── ReactJs.js
│ ├── Sass.js
│ ├── Supabase.js
│ ├── Tailwind.js
│ └── TwitterProfile.js
└── structure
│ └── Header.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── api
│ └── hello.js
└── index.js
├── postcss.config.js
├── public
├── bg.svg
├── diamond.svg
├── favicon.ico
├── favicon.png
├── headshot-2023.jpg
├── headshot-with-frame-2.jpg
├── headshot-with-frame.jpg
├── headshot.jpg
├── html.svg
├── icon.svg
├── logo-02.svg
├── logo-faded.svg
├── logo.svg
├── projects
│ ├── butlr.png
│ ├── colorhub.png
│ ├── gps-embroidery.png
│ ├── profileme.png
│ ├── quotr.png
│ ├── ratemyfilm.png
│ ├── reportr.png
│ ├── smylo.png
│ ├── spotlight.png
│ └── yodlr.png
└── videos
│ └── landing-page-video.mp4
├── styles
└── globals.css
├── tailwind.config.js
└── utils
└── constants.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Portfolio - danielcranney.com
3 | ## Project description
4 | My portfolio website, built with NextJS, React and TailwindCSS.
5 |
6 |
7 | ## Installation
8 |
9 | **Install dependencies**
10 | ```bash
11 | npm install
12 | ```
13 |
14 | **Run development server**
15 | ```bash
16 | npm run dev
17 | ```
18 |
19 | ## Packages
20 |
21 | - [Next.js](https://nextjs.org/docs)
22 | - [Tailwindcss](https://tailwindcss.com/docs)
23 |
24 | ## License
25 | [GNU General Public License v3.0](https://choosealicense.com/licenses/gpl-3.0/)
26 |
--------------------------------------------------------------------------------
/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Button = ({ link, text, icon, square }) => {
4 | return (
5 |
6 |
11 | {icon}
12 | {text ? (
13 |
16 | {text}
17 |
18 | ) : null}
19 |
20 |
21 | );
22 | };
23 |
24 | export default Button;
25 |
--------------------------------------------------------------------------------
/components/FeaturedProjectCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import MockupToolbar from "./MockupToolbar";
3 | import Button from "./Button";
4 | import SourceButton from "./SourceButton";
5 | import Image from "next/image";
6 |
7 | import GitHub from "./icons/GitHub";
8 | import ExternalLink from "./icons/ExternalLink";
9 |
10 | const FeaturedProjectCard = ({
11 | title,
12 | status,
13 | float,
14 | flexDirection,
15 | description,
16 | imgWidth,
17 | imgHeight,
18 | imgSrc,
19 | stack,
20 | liveLink,
21 | repoLink,
22 | }) => {
23 | return (
24 |
27 | {/* Project image */}
28 |
29 |
30 |
36 |
37 |
38 | {/* Project info */}
39 |
42 |
43 | {status}
44 |
45 |
{title}
46 |
47 |
{stack}
48 |
49 | {description}
50 |
51 |
52 | {liveLink !== null ? (
53 | }
58 | />
59 | ) : null}
60 |
61 | {repoLink !== null ? (
62 | }
67 | />
68 | ) : null}
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default FeaturedProjectCard;
76 |
--------------------------------------------------------------------------------
/components/Icon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Icon = ({
4 | IconType,
5 | padding,
6 | width,
7 | height,
8 | flexDirection,
9 | title,
10 | titleMargins,
11 | titleSize,
12 | marginBottom,
13 | marginRight,
14 | textTransform,
15 | fixedHeight,
16 | }) => {
17 | return (
18 |
21 |
24 |
25 |
26 | {title ? (
27 | <>
28 | {/*
31 | {title}
32 |
33 | ) : null} */}
34 | >
35 | ) : null}
36 |
37 | );
38 | };
39 |
40 | export default Icon;
41 |
--------------------------------------------------------------------------------
/components/MockupToolbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const MockupToolbar = () => {
4 | return (
5 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
27 |
28 |
29 | );
30 | };
31 | export default MockupToolbar;
32 |
--------------------------------------------------------------------------------
/components/NewIcon.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NewIcon = ({
4 | IconType,
5 | padding,
6 | width,
7 | height,
8 | flexDirection,
9 | title,
10 | titleMargins,
11 | titleSize,
12 | marginBottom,
13 | marginRight,
14 | textTransform,
15 | fixedHeight,
16 | }) => {
17 | return (
18 |
23 |
35 |
36 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default NewIcon;
52 |
--------------------------------------------------------------------------------
/components/ProjectCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ExternalLink from "./icons/ExternalLink";
3 | import GitHub from "./icons/GitHub";
4 | import Button from "./Button";
5 |
6 | const ProjectCard = ({ project }) => {
7 | const { title, overview, stack, link, repo, isSiteLive } = project;
8 |
9 | return (
10 |
11 |
18 |
22 |
26 |
35 |
36 |
37 | {title}
38 |
39 |
{overview}
40 | {/* Stack icons inner container */}
41 |
42 |
43 | {stack.map(function (stackItem, i) {
44 | return (
45 |
49 | {stackItem}
50 |
51 | );
52 | })}
53 |
54 |
55 | {/* Live and GitHub container */}
56 |
57 | {link ? (
58 |
}
63 | />
64 | ) : null}
65 |
66 | {repo ? (
67 |
}
72 | />
73 | ) : null}
74 |
75 | {!isSiteLive ?
Coming soon
: null}
76 |
77 |
78 | );
79 | };
80 |
81 | export default ProjectCard;
82 |
--------------------------------------------------------------------------------
/components/SourceButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import GitHub from "./icons/GitHub";
3 |
4 | const SourceButton = ({ sourceLink }) => {
5 | return (
6 |
12 |
13 |
14 |
15 | Source
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default SourceButton;
23 |
--------------------------------------------------------------------------------
/components/blog/BlogItem.js:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | export default function BlogItem({ post }) {
3 | const redirectToHashnode = () => {
4 | window.open("https://blog.danielcranney.com/" + post.slug, "_blank");
5 | };
6 |
7 | const getDateAdded = () => {
8 | let d = new Date(post.dateAdded);
9 | let ye = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d);
10 | let mo = new Intl.DateTimeFormat("en", { month: "short" }).format(d);
11 | let da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d);
12 | return `${mo} ${da}, ${ye}`;
13 | };
14 |
15 | const formattedDate = getDateAdded();
16 |
17 | return (
18 |
22 |
23 |
29 |
30 |
31 |
{post.title}
32 |
33 | {formattedDate}
34 |
35 |
{post.brief.substr(0, 150)}...
36 |
Read more
37 |
38 |
39 |
40 |
41 |
Open
42 |
49 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/components/blog/BlogList.js:
--------------------------------------------------------------------------------
1 | import BlogItem from "./BlogItem";
2 |
3 | export default function BlogList({ publications }) {
4 | let posts = publications.data.user.publication.posts;
5 |
6 | return (
7 |
8 | {posts.map((post, index) => {
9 | return (
10 |
11 |
12 |
13 | );
14 | })}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/components/icons/AdobeXd.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AdobeXd = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default AdobeXd;
15 |
--------------------------------------------------------------------------------
/components/icons/AfterEffects.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const AfterEffects = () => {
4 | return (
5 |
6 |
7 |
11 |
15 |
16 | );
17 | };
18 | export default AfterEffects;
19 |
--------------------------------------------------------------------------------
/components/icons/Bootstrap.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Bootstrap = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Bootstrap;
15 |
--------------------------------------------------------------------------------
/components/icons/Css.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Css = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Css;
15 |
--------------------------------------------------------------------------------
/components/icons/Express.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Express = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Express;
15 |
--------------------------------------------------------------------------------
/components/icons/ExternalLink.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ExternalLink = ({ square }) => {
4 | return (
5 |
14 |
20 |
21 | );
22 | };
23 |
24 | export default ExternalLink;
25 |
--------------------------------------------------------------------------------
/components/icons/Figma.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Figma = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Figma;
15 |
--------------------------------------------------------------------------------
/components/icons/Firebase.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function Firebase() {
4 | return (
5 |
6 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/components/icons/GitHub.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const GitHub = ({ square }) => {
4 | return (
5 |
11 |
16 |
21 |
22 |
23 |
24 | );
25 | };
26 | export default GitHub;
27 |
--------------------------------------------------------------------------------
/components/icons/GitHubProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const GitHubProfile = ({ marginBottom }) => {
4 | return (
5 |
22 | );
23 | };
24 |
25 | export default GitHubProfile;
26 |
--------------------------------------------------------------------------------
/components/icons/Html.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Html = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Html;
15 |
--------------------------------------------------------------------------------
/components/icons/Illustrator.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function Illustrator() {
4 | return (
5 |
6 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/icons/Javascript.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Javascript = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Javascript;
15 |
--------------------------------------------------------------------------------
/components/icons/LinkedInProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const LinkedInProfile = ({ marginBottom }) => {
4 | return (
5 |
22 | );
23 | };
24 |
25 | export default LinkedInProfile;
26 |
--------------------------------------------------------------------------------
/components/icons/MongoDb.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const MongoDb = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default MongoDb;
15 |
--------------------------------------------------------------------------------
/components/icons/NextJs.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NextJs = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default NextJs;
15 |
--------------------------------------------------------------------------------
/components/icons/NodeJs.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NodeJs = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default NodeJs;
15 |
--------------------------------------------------------------------------------
/components/icons/Photoshop.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Photoshop = () => {
4 | return (
5 |
6 |
12 |
13 | );
14 | };
15 |
16 | export default Photoshop;
17 |
--------------------------------------------------------------------------------
/components/icons/ReactJs.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const ReactJs = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default ReactJs;
15 |
--------------------------------------------------------------------------------
/components/icons/Sass.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Sass = () => {
4 | return (
5 |
6 |
12 |
13 | );
14 | };
15 |
16 | export default Sass;
17 |
--------------------------------------------------------------------------------
/components/icons/Supabase.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Supabase = () => {
4 | return (
5 |
6 |
10 |
11 | );
12 | };
13 |
14 | export default Supabase;
15 |
--------------------------------------------------------------------------------
/components/icons/Tailwind.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Tailwind = () => {
4 | return (
5 |
6 |
11 |
12 | );
13 | };
14 |
15 | export default Tailwind;
16 |
--------------------------------------------------------------------------------
/components/icons/TwitterProfile.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const TwitterProfile = ({ marginBottom }) => {
4 | return (
5 |
22 | );
23 | };
24 |
25 | export default TwitterProfile;
26 |
--------------------------------------------------------------------------------
/components/structure/Header.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/components/structure/Header.js
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | remotePatterns: [
4 | {
5 | protocol: "https",
6 | hostname: "cdn.hashnode.com",
7 | port: "",
8 | },
9 | ],
10 | },
11 | reactStrictMode: true,
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "portfolio-2021",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "13.2.4",
13 | "next-themes": "^0.2.0",
14 | "react": "18.2.0",
15 | "react-dom": "18.2.0",
16 | "react-typing-effect": "^2.0.5"
17 | },
18 | "devDependencies": {
19 | "@svgr/webpack": "^5.5.0",
20 | "@tailwindcss/aspect-ratio": "^0.4.0",
21 | "@tailwindcss/forms": "^0.5.1",
22 | "@tailwindcss/line-clamp": "^0.4.0",
23 | "@tailwindcss/typography": "^0.5.2",
24 | "autoprefixer": "^10.4.7",
25 | "eslint": "7.32.0",
26 | "eslint-config-next": "11.0.1",
27 | "postcss": "^8.4.13",
28 | "tailwindcss": "^3.0.24"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 | import { ThemeProvider } from "next-themes";
3 | import { useTheme } from "next-themes";
4 |
5 | function MyApp({ Component, pageProps }) {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
13 | export default MyApp
14 |
--------------------------------------------------------------------------------
/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default function handler(req, res) {
4 | res.status(200).json({ name: 'John Doe' })
5 | }
6 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import Head from "next/head";
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import ReactTypingEffect from "react-typing-effect";
6 |
7 | import Icon from "../components/Icon";
8 | // Icons
9 | import Html from "../components/icons/Html";
10 | import Css from "../components/icons/Css";
11 | import Javascript from "../components/icons/Javascript";
12 | import Tailwind from "../components/icons/Tailwind";
13 | import Bootstrap from "../components/icons/Bootstrap";
14 | import Sass from "../components/icons/Sass";
15 | import ReactJs from "../components/icons/ReactJs";
16 | import NextJs from "../components/icons/NextJs";
17 | import NodeJs from "../components/icons/NodeJs";
18 | import Firebase from "../components/icons/Firebase";
19 | import Figma from "../components/icons/Figma";
20 | import Photoshop from "../components/icons/Photoshop";
21 | import Illustrator from "../components/icons/Illustrator";
22 | import AfterEffects from "../components/icons/AfterEffects";
23 | import AdobeXd from "../components/icons/AdobeXd";
24 | import Supabase from "../components/icons/Supabase";
25 | import MongoDb from "../components/icons/MongoDb";
26 | import Express from "../components/icons/Express";
27 | // Project Card
28 | import ProjectCard from "../components/ProjectCard";
29 | import GitHubProfile from "../components/icons/GitHubProfile";
30 | import TwitterProfile from "../components/icons/TwitterProfile";
31 | import LinkedInProfile from "../components/icons/LinkedInProfile";
32 | import FeaturedProjectCard from "../components/FeaturedProjectCard";
33 |
34 | // Blog Components
35 | import BlogList from "../components/blog/BlogList";
36 | import BlogItem from "../components/blog/BlogItem";
37 |
38 | // Dark Mode
39 | import { useTheme } from "next-themes";
40 |
41 | import { projects } from "../utils/constants";
42 | import NewIcon from "../components/NewIcon";
43 |
44 | const getDimensions = (ele) => {
45 | const { height } = ele.getBoundingClientRect();
46 | const offsetTop = ele.offsetTop;
47 | const offsetBottom = offsetTop + height;
48 |
49 | return {
50 | height,
51 | offsetTop,
52 | offsetBottom,
53 | };
54 | };
55 |
56 | const scrollTo = (ele) => {
57 | ele.scrollIntoView({
58 | behavior: "smooth",
59 | block: "start",
60 | });
61 | };
62 |
63 | export default function Home({ publications }) {
64 | const [visibleSection, setVisibleSection] = useState();
65 | const [scrolling, setScrolling] = useState(false);
66 | const [scrollPosition, setScrollPosition] = useState(0);
67 | const [navbarOpen, setNavbarOpen] = useState(false);
68 | const [showHeader, setShowHeader] = useState(true);
69 | const [isMobile, setIsMobile] = useState(false);
70 | const [mounted, setMounted] = useState(false);
71 | const { systemTheme, theme, setTheme } = useTheme();
72 |
73 | const handleResize = () => {
74 | if (window.innerWidth < 1024) {
75 | } else {
76 | setNavbarOpen(false);
77 | }
78 | };
79 |
80 | const headerRef = useRef(null);
81 | const homeRef = useRef(null);
82 | const aboutRef = useRef(null);
83 | const skillsRef = useRef(null);
84 | const myWorkRef = useRef(null);
85 | const blogRef = useRef(null);
86 | const contactRef = useRef(null);
87 |
88 | useEffect(() => {
89 | const sectionRefs = [
90 | { section: "home", ref: homeRef, id: 1 },
91 | { section: "about", ref: aboutRef, id: 2 },
92 | { section: "skills", ref: skillsRef, id: 3 },
93 | { section: "my-work", ref: myWorkRef, id: 4 },
94 | { section: "blog", ref: blogRef, id: 5 },
95 | { section: "contact", ref: contactRef, id: 6 },
96 | ];
97 |
98 | const handleScroll = () => {
99 | const { height: headerHeight } = getDimensions(headerRef.current);
100 | const scrollPosition = window.scrollY + headerHeight;
101 |
102 | const selected = sectionRefs.find(({ section, ref }) => {
103 | const ele = ref.current;
104 | if (ele) {
105 | const { offsetBottom, offsetTop } = getDimensions(ele);
106 | return scrollPosition >= offsetTop && scrollPosition <= offsetBottom;
107 | }
108 | });
109 |
110 | if (selected && selected.section !== visibleSection) {
111 | setVisibleSection(selected.section);
112 | // console.log(visibleSection);
113 | } else if (!selected && visibleSection) {
114 | setVisibleSection(undefined);
115 | }
116 | };
117 |
118 | handleScroll();
119 | window.addEventListener("scroll", handleScroll);
120 | return () => {
121 | window.removeEventListener("scroll", handleScroll);
122 | };
123 | }, [visibleSection]);
124 |
125 | // Handle Header Scroll Away
126 | useEffect(() => {
127 | let prevScrollPos = window.pageYOffset;
128 | let timeoutId;
129 |
130 | const handleScrollBack = () => {
131 | const currentScrollPos = window.pageYOffset;
132 |
133 | // Delay before the header scrolls back into view
134 | if (currentScrollPos < scrollPosition - 50 && scrolling) {
135 | timeoutId = setTimeout(() => {
136 | setShowHeader(true);
137 | }, 250);
138 | }
139 |
140 | // Add an easing effect when the header scrolls into and out of view
141 | if (currentScrollPos > prevScrollPos + 10 && !scrolling && showHeader) {
142 | setScrolling(true);
143 | } else if (currentScrollPos < prevScrollPos - 10 && scrolling) {
144 | clearTimeout(timeoutId);
145 | setScrolling(false);
146 | setTimeout(() => {
147 | setShowHeader(false);
148 | }, 250);
149 | }
150 |
151 | prevScrollPos = currentScrollPos;
152 | };
153 |
154 | window.addEventListener("scroll", handleScrollBack);
155 | return () => window.removeEventListener("scroll", handleScrollBack);
156 | }, [scrollPosition, scrolling, showHeader]);
157 |
158 | useEffect(() => {
159 | if (typeof window !== "undefined") {
160 | window.addEventListener("scroll", () =>
161 | setScrolling(window.pageYOffset > 110)
162 | );
163 | }
164 | }, []);
165 |
166 | useEffect(() => {
167 | window.addEventListener("resize", handleResize);
168 | });
169 |
170 | useEffect(() => {
171 | setMounted(true);
172 | }, []);
173 |
174 | const currentTheme = theme === "system" ? systemTheme : theme;
175 |
176 | useEffect(() => {
177 | console.log(currentTheme);
178 | }, [currentTheme]);
179 |
180 | const renderThemeChanger = () => {
181 | if (!mounted) return null;
182 |
183 | if (currentTheme === "dark") {
184 | return (
185 |
191 |
197 |
198 | );
199 | } else {
200 | return (
201 |
207 |
213 |
214 | );
215 | }
216 | };
217 |
218 | return (
219 |
220 |
225 |
226 |
Daniel Cranney | Frontend Developer & Designer
227 |
231 |
232 |
233 |
234 | {/* Full-screen Menu */}
235 |
242 |
243 |
244 |
245 |
246 | {
254 | setNavbarOpen(false);
255 | scrollTo(homeRef.current);
256 | }}
257 | >
258 | Home
259 |
260 |
261 |
262 | {
270 | setNavbarOpen(false);
271 | scrollTo(aboutRef.current);
272 | }}
273 | >
274 | About
275 |
276 |
277 |
278 | {
286 | setNavbarOpen(false);
287 | scrollTo(skillsRef.current);
288 | }}
289 | >
290 | Skills
291 |
292 |
293 |
294 | {
302 | setNavbarOpen(false);
303 | scrollTo(myWorkRef.current);
304 | }}
305 | >
306 | My Work
307 |
308 |
309 |
310 | {
320 | setNavbarOpen(false);
321 | scrollTo(blogRef.current);
322 | }}
323 | >
324 | Blog
325 |
326 |
327 |
328 | {
336 | setNavbarOpen(false);
337 | scrollTo(contactRef.current);
338 | }}
339 | >
340 | Contact
341 |
342 |
343 |
344 |
348 | Get in touch
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 | {/* Header and Nav */}
357 |
512 |
513 | {/* Content Container */}
514 |
515 | {/* Hero Content */}
516 |
517 | {/* Main */}
518 |
519 |
520 | {/*
521 | Hello! 👋 My name is
522 | */}
523 |
524 |
525 | Daniel Cranney
526 |
527 |
528 |
540 |
541 |
542 | I design and build websites that look good, and work well.
543 |
544 |
{
547 | scrollTo(myWorkRef.current);
548 | }}
549 | >
550 | See my Work
551 |
552 |
553 |
554 |
555 |
556 | {/* About */}
557 |
562 |
563 |
About
564 |
565 |
566 |
567 |
568 |
569 | Hi! I'm Dan and I'm a frontend developer, designer
570 | and teacher from Bristol, England.
571 |
572 |
573 | After building my first website aged thirteen, I knew I
574 | wanted to work with computers and technology, and I've
575 | never looked back.
576 |
577 |
578 | After graduating University with a Media degree, I began
579 | freelancing as a designer, creating graphics, video content
580 | and websites for small businesses, using content management
581 | systems like Wordpress, Joomla and Squarespace.
582 |
583 |
584 | In recent years, I've been focused on programming,
585 | building a solid frontend stack and creating exciting
586 | projects that solve real-world problems.
587 |
588 |
589 | Alongside my design and development work, I run a BA Media
590 | Production degree course and a corporate video production
591 | company called{" "}
592 |
598 | Spotlight Media
599 |
600 | , so I like to keep busy!
601 |
602 |
603 | Take a look at my work below to see what I'm working
604 | on, and get in touch if you'd like to work together!
605 |
606 |
607 |
608 |
615 |
616 |
617 |
618 |
619 |
620 | {/* Skills */}
621 |
626 | Skills
627 |
628 |
629 | {/* Skills icons */}
630 |
631 | {/* HTML */}
632 |
646 |
647 | {/* CSS */}
648 |
662 |
663 | {/* Tailwind */}
664 |
678 |
679 | {/* Javascript */}
680 |
694 |
695 | {/* React */}
696 |
710 |
711 | {/* Next */}
712 |
726 |
727 | {/* Node */}
728 |
742 |
743 | {/* Express */}
744 |
758 |
759 | {/* Supabase */}
760 |
774 |
775 | {/* MongoDb */}
776 |
790 |
791 | {/* Sass */}
792 |
806 |
807 | {/* Bootstrap */}
808 | {/* */}
822 |
823 | {/* Firebase */}
824 |
838 |
839 | {/* Photoshop */}
840 |
854 |
855 | {/* Illustrator */}
856 |
870 |
871 | {/* After Effects */}
872 |
886 |
887 | {/* Adobe XD */}
888 |
902 |
903 |
904 |
905 | {/* My Work */}
906 |
911 | {/* My Work header */}
912 | My Work
913 |
914 |
915 | {/* Featured Projects Container */}
916 |
917 | {/* Project One */}
918 |
931 |
946 |
947 |
962 |
963 |
978 |
979 |
994 |
995 |
1010 | >
1011 | }
1012 | />
1013 | {/* Project Two */}
1014 |
1027 |
1042 |
1043 |
1058 |
1059 |
1074 |
1075 |
1090 | >
1091 | }
1092 | />
1093 | {/* Project Three */}
1094 |
1107 |
1122 |
1123 |
1138 |
1139 |
1154 |
1155 |
1170 | >
1171 | }
1172 | />
1173 |
1174 |
1175 | {/* Other Projects header */}
1176 | Other Projects
1177 |
1178 |
1179 | Check out some of the projects I've been a part of...
1180 |
1181 |
1182 | {/* Other Projects Container */}
1183 |
1184 | {projects.map(function (project, i) {
1185 | return
;
1186 | })}
1187 |
1188 |
1189 |
1190 | {/* Blog */}
1191 | {/*
1196 | Blog
1197 |
1198 |
1199 |
1200 | */}
1201 |
1202 | {/* Contact */}
1203 |
1230 |
1231 | {/* Footer */}
1232 |
1280 |
1281 |
1282 | {/* Fixed Container */}
1283 |
1284 |
1285 | {/* Profile Icons */}
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 | {/* Pagination */}
1294 |
1295 | {/* Hero - Diamond 1 */}
1296 |
{
1299 | scrollTo(homeRef.current);
1300 | }}
1301 | >
1302 |
1314 | {/* Fill */}
1315 |
1323 | {/* Border */}
1324 |
1332 |
1333 |
1334 | {/* About - Diamond 2 */}
1335 |
{
1338 | scrollTo(aboutRef.current);
1339 | }}
1340 | >
1341 |
1353 | {/* Fill */}
1354 |
1362 | {/* Border */}
1363 |
1371 |
1372 |
1373 | {/* Skills - Diamond 3 */}
1374 |
{
1377 | scrollTo(skillsRef.current);
1378 | }}
1379 | >
1380 |
1392 | {/* Fill */}
1393 |
1401 | {/* Border */}
1402 |
1410 |
1411 |
1412 | {/* My Work - Diamond 4 */}
1413 |
{
1416 | scrollTo(myWorkRef.current);
1417 | }}
1418 | >
1419 |
1431 | {/* Fill */}
1432 |
1440 | {/* Border */}
1441 |
1449 |
1450 |
1451 | {/* Blog - Diamond 5 */}
1452 |
{
1455 | scrollTo(blogRef.current);
1456 | }}
1457 | >
1458 |
1470 | {/* Fill */}
1471 |
1479 | {/* Border */}
1480 |
1488 |
1489 |
1490 | {/* Contact - Diamond 6 */}
1491 |
{
1494 | scrollTo(contactRef.current);
1495 | }}
1496 | >
1497 |
1509 | {/* Fill */}
1510 |
1518 | {/* Border */}
1519 |
1527 |
1528 |
1529 |
1530 | {/* Line */}
1531 |
1532 |
1533 |
1534 |
1535 |
1536 |
1537 | );
1538 | }
1539 |
1540 | // export async function getServerSideProps(context) {
1541 | // const res = await fetch("https://api.hashnode.com/", {
1542 | // method: "POST",
1543 | // headers: {
1544 | // "Content-Type": "application/json",
1545 | // Authorization: "32ab9fe7-0331-4efc-bdb8-5a3e0bfdd9b9",
1546 | // },
1547 | // body: JSON.stringify({
1548 | // query:
1549 | // 'query {user(username: "danielcranney") {publication {posts(page: 0) {title brief slug coverImage dateAdded}}}}',
1550 | // }),
1551 | // });
1552 | // const publications = await res.json();
1553 |
1554 | // if (!publications) {
1555 | // return {
1556 | // notFound: true,
1557 | // };
1558 | // }
1559 |
1560 | // return {
1561 | // props: {
1562 | // publications,
1563 | // },
1564 | // };
1565 | // }
1566 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/bg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/diamond.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/favicon.png
--------------------------------------------------------------------------------
/public/headshot-2023.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/headshot-2023.jpg
--------------------------------------------------------------------------------
/public/headshot-with-frame-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/headshot-with-frame-2.jpg
--------------------------------------------------------------------------------
/public/headshot-with-frame.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/headshot-with-frame.jpg
--------------------------------------------------------------------------------
/public/headshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/headshot.jpg
--------------------------------------------------------------------------------
/public/html.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo-02.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo-faded.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/projects/butlr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/butlr.png
--------------------------------------------------------------------------------
/public/projects/colorhub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/colorhub.png
--------------------------------------------------------------------------------
/public/projects/gps-embroidery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/gps-embroidery.png
--------------------------------------------------------------------------------
/public/projects/profileme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/profileme.png
--------------------------------------------------------------------------------
/public/projects/quotr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/quotr.png
--------------------------------------------------------------------------------
/public/projects/ratemyfilm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/ratemyfilm.png
--------------------------------------------------------------------------------
/public/projects/reportr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/reportr.png
--------------------------------------------------------------------------------
/public/projects/smylo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/smylo.png
--------------------------------------------------------------------------------
/public/projects/spotlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/spotlight.png
--------------------------------------------------------------------------------
/public/projects/yodlr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/projects/yodlr.png
--------------------------------------------------------------------------------
/public/videos/landing-page-video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danielcranney/portfolio-v1/60fe61584a2974b3cfca75185c3caaa803152eb4/public/videos/landing-page-video.mp4
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import url("https://use.typekit.net/ewi2vgp.css");
2 |
3 | /* ./styles/globals.css */
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | @layer base {
9 | * {
10 | @apply antialiased;
11 | }
12 | body {
13 | @apply bg-light/10 dark:bg-dark text-white;
14 | }
15 | }
16 |
17 | /* Typography Styles */
18 | @layer base {
19 | h1,
20 | h2,
21 | h3,
22 | h4,
23 | h5,
24 | h6 {
25 | @apply font-bold mb-2 tracking-tight text-darker dark:text-white;
26 | }
27 | h1 {
28 | @apply font-display text-4xl md:text-5xl;
29 | }
30 | h2 {
31 | @apply font-display text-3xl md:text-4xl;
32 | }
33 | h3 {
34 | @apply font-display text-2xl md:text-3xl;
35 | }
36 | h4 {
37 | @apply font-display text-xl md:text-2xl;
38 | }
39 | h5 {
40 | @apply font-display text-lg md:text-xl;
41 | }
42 | h6 {
43 | @apply font-display text-sm md:text-base;
44 | }
45 | .small-text {
46 | @apply text-sm font-semibold uppercase dark:text-light text-dark;
47 | }
48 | p {
49 | @apply font-body text-base mb-4 leading-relaxed
50 | dark:text-light text-dark font-normal;
51 | }
52 | a {
53 | @apply font-semibold text-brand opacity-100 transition-all duration-200 hover:text-white;
54 | }
55 | }
56 |
57 | @layer components {
58 | .nav-item {
59 | @apply relative after:absolute after:bg-brand after:bottom-0 after:left-0 after:h-[2px] after:w-full after:origin-bottom-right after:transition-transform after:ease-in-out after:duration-300 font-bold leading-loose cursor-pointer dark:text-light dark:hover:text-white text-mid hover:text-darker font-body;
60 | }
61 | .nav-item.active {
62 | @apply after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100;
63 | }
64 | .nav-item.current {
65 | @apply after:scale-x-100 after:origin-bottom-left text-darker dark:text-white;
66 | }
67 | }
68 |
69 | @layer base {
70 | .underline-link {
71 | @apply hover:border-brand border-b-2 border-transparent;
72 | }
73 | }
74 |
75 | @layer components {
76 | button {
77 | @apply font-body;
78 | }
79 | }
80 | /* Large Button */
81 | @layer components {
82 | .btn-lg {
83 | @apply inline-flex h-[3.25rem] items-center px-5 justify-center
84 | text-center
85 | font-semibold rounded-lg
86 | shadow-none
87 | self-start transition-all duration-150 ease-in-out;
88 | }
89 | }
90 |
91 | /* Medium Button */
92 | @layer components {
93 | .btn-md {
94 | @apply inline-flex h-12 py-1 items-center justify-center px-6 flex-grow-0
95 | text-center
96 | font-semibold rounded-lg
97 | shadow-none
98 | text-base
99 | transition-all duration-300 ease-in-out;
100 | }
101 | }
102 |
103 | /* XS Button */
104 | @layer components {
105 | .btn-xs {
106 | @apply inline-flex h-10 items-center justify-center
107 | px-3.5 flex-grow-0
108 | text-center
109 | font-semibold rounded-sm
110 | shadow-none
111 | text-sm
112 | transition-all duration-150 ease-in-out;
113 | }
114 | }
115 |
116 | /* Small Button */
117 | @layer components {
118 | .btn-sm {
119 | @apply inline-flex h-12 items-center justify-center
120 | px-5 flex-grow-0
121 | text-center
122 | font-semibold rounded-sm
123 | shadow-none
124 | text-sm
125 | transition-all duration-150 ease-in-out;
126 | }
127 | }
128 | @layer components {
129 | .btn-sm-no-text {
130 | @apply inline-flex h-12 items-center justify-center
131 | flex-grow-0
132 | text-center
133 | font-semibold rounded-sm
134 | shadow-none
135 | text-sm
136 | transition-all duration-150 ease-in-out;
137 | }
138 | }
139 | /* Standard Buttons */
140 | @layer components {
141 | .btn-brand {
142 | @apply border-2
143 | bg-brand
144 | border-brand
145 | hover:bg-brandAlt
146 | hover:border-brandAlt
147 | dark:text-darker text-darker hover:text-darker
148 | focus:outline-none
149 | focus:ring-0
150 | transition-all duration-150 ease-in-out;
151 | }
152 | }
153 |
154 | /* Outline buttons */
155 | @layer components {
156 | .btn-outline {
157 | @apply border-2
158 | bg-transparent
159 | border-white
160 | hover:border-brand
161 | hover:bg-brand
162 | text-white
163 | focus:outline-none
164 | focus:ring-0
165 | transition-all duration-150 ease-in-out;
166 | }
167 | }
168 |
169 | @layer base {
170 | .selected {
171 | @apply border-brand dark:text-white text-dark group-hover:text-darker border-b-2 opacity-100 dark:hover:border-white hover:border-darker;
172 | }
173 | }
174 |
175 | @layer components {
176 | input,
177 | textarea {
178 | @apply w-full bg-darker rounded-sm appearance-none border-0 p-4 text-white focus:outline-none
179 | focus:ring-2 focus:ring-brand;
180 | }
181 | }
182 |
183 | html {
184 | scroll-behavior: smooth;
185 | }
186 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind.config.js
2 | const colors = require("tailwindcss/colors");
3 |
4 | module.exports = {
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx}",
7 | "./components/**/*.{js,ts,jsx,tsx}",
8 | ],
9 | darkMode: "class",
10 | theme: {
11 | container: {
12 | padding: {
13 | DEFAULT: "1rem",
14 | sm: "2rem",
15 | lg: "4rem",
16 | xl: "2rem",
17 | "2xl": "2rem",
18 | },
19 | },
20 | extend: {
21 | fontFamily: {
22 | display: ["termina", "sans-serif"],
23 | body: ['"neue-haas-grotesk-text"', "sans-serif"],
24 | },
25 | width: {
26 | "30pc": "30%",
27 | "31pc": "31%",
28 | "32pc": "32%",
29 | "49pc": "49%",
30 | "48pc": "48%",
31 | "1/8": "12.5%",
32 | "2/8": "25%",
33 | "3/8": "37.5%",
34 | "4/8": "50%",
35 | "5/8": "65.8%",
36 | "6/8": "75%",
37 | "7/8": "87.5%",
38 | },
39 | transitionProperty: {
40 | width: "width",
41 | },
42 | colors: {
43 | soft: "#f0f0f0",
44 | brandAlt: "#e4bc3b",
45 | brand: "#DFB537",
46 | darker: "#0C0C0D",
47 | dark: "#2F2E33",
48 | mid: "#827F8B",
49 | light: "#D4CFDE",
50 | lightest: "#FFFFFF",
51 | },
52 | backgroundImage: (theme) => ({
53 | "header-img": "url('/bg.svg')",
54 | }),
55 | },
56 | },
57 | plugins: [],
58 | };
59 |
--------------------------------------------------------------------------------
/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const projects = [
2 | {
3 | title: "Reportr",
4 | overview: "Write reports for your students in 60 seconds or less",
5 | stack: ["Html", "React", "Next", "Supabase"],
6 | link: "http://www.reportr.io",
7 | repo: null,
8 | isSiteLive: true,
9 | },
10 | {
11 | title: "Yodlr (Archived)",
12 | overview:
13 | "Shoutout a Twitter user, and generate a custom profile card in under a minute. Update: Since Twitter announced new API pricing in 2023, this project no longer functions correctly, and has been archived.",
14 | stack: ["Html", "Tailwind", "React", "Next"],
15 | link: "http://yodlr.vercel.app",
16 | repo: "https://github.com/danielcranney/yodlr",
17 | isSiteLive: true,
18 | },
19 | {
20 | title: "Rate My Film",
21 | overview:
22 | "A single-page application that helps filmmakers learn more about who their film might be suitable for.",
23 | stack: ["Html", "React", "Sass"],
24 | link: "http://www.ratemyfilm.co.uk",
25 | repo: "https://github.com/danielcranney/rate-my-film",
26 | isSiteLive: true,
27 | },
28 | {
29 | title: "Spotlight Media",
30 | overview:
31 | "The website for my corporate videography company. This features a contact form powered by NodeJs and SendGrid.",
32 | stack: ["Html", "ReactJs", "Next", "Node"],
33 | link: "http://www.wearespotlight.co.uk",
34 | repo: "https://github.com/danielcranney/spotlight-media",
35 | isSiteLive: true,
36 | },
37 | {
38 | title: "Quotr",
39 | overview:
40 | "A simple application built to help tradespeople automate the quotation process.",
41 | stack: ["Html", "React", "Next"],
42 | link: "https://quotr.vercel.app",
43 | repo: null,
44 | isSiteLive: true,
45 | },
46 | {
47 | title: "GPS Embroidery",
48 | overview:
49 | "A fully-responsive and quick-rendering image-based website for an ongoing academic project.",
50 | stack: ["Html", "React", "Next"],
51 | link: "http://www.gps-embroidery.com",
52 | repo: "https://github.com/danielcranney/GPS-Embroidery",
53 | isSiteLive: true,
54 | },
55 | ];
56 |
--------------------------------------------------------------------------------