diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..77c6b79 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# These are retrieved from your project at app.tina.io +NEXT_PUBLIC_TINA_CLIENT_ID=*** +TINA_TOKEN=*** + +# This is set by default CI with Netlify/Vercel/Github, but can be overriden +NEXT_PUBLIC_TINA_BRANCH=*** +# To see the preview functionality +VERCEL_ENV=preview \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7fdfab2 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "eslint:recommended", + "next" + ] +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..204cc47 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + allow: + - dependency-name: "tinacms" + - dependency-name: "@tinacms*" diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml new file mode 100644 index 0000000..f42b61a --- /dev/null +++ b/.github/workflows/pr-open.yml @@ -0,0 +1,23 @@ +name: Build Pull request +on: + pull_request: + types: [opened, synchronize, reopened] +env: + NEXT_PUBLIC_TINA_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_TINA_CLIENT_ID }} + TINA_TOKEN: ${{ secrets.TINA_TOKEN }} + NEXT_PUBLIC_TINA_BRANCH: ${{ github.head_ref }} +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node: ["18"] + name: Node ${{ matrix.node }} sample + steps: + - uses: actions/checkout@v3 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + - run: yarn install + - run: yarn build diff --git a/.github/workflows/update-dependabot-pr.yml b/.github/workflows/update-dependabot-pr.yml new file mode 100644 index 0000000..fd620eb --- /dev/null +++ b/.github/workflows/update-dependabot-pr.yml @@ -0,0 +1,32 @@ +name: Update TinaCMS Dependencies +on: + push: + branches: + - dependabot/npm_and_yarn/** + +jobs: + update-tinacms: + runs-on: ubuntu-latest + steps: + # Clone repo + - name: Check out repo to update + uses: actions/checkout@v2 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + # Install deps, update Tina packages, update schema + - name: Install dependencies + run: yarn install + - name: Update Tina packages + run: yarn upgrade tinacms@latest @tinacms/cli@latest + - name: Update Schema + run: yarn tinacms audit + # Commit changes + - name: Commit changes back to branch + uses: EndBug/add-and-commit@v9 + with: + message: "Update TinaCMS generated files" + branch: ${{ github.ref }} + committer_name: GitHub Actions + committer_email: actions@github.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c0779d --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +.env +.env.local +.idea + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.tina/__generated__ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f35faae --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "graphql.vscode-graphql", + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..6f68561 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Tina Cloud Starter +Copyright 2024 SSW + +This product includes software developed at SSW (http://www.ssw.com.au/). diff --git a/README.md b/README.md index 803e476..acbc565 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ -# TinaCMS + NextJs Template -This project template will be used to onboard new developers. It will also serve as a default template for any CMS related websites. +# Tina Starter 🦙 + +![tina-cloud-starter-demo](https://user-images.githubusercontent.com/103008/130587027-995ccc45-a852-4f90-b658-13e8e0517339.gif) + +This Next.js starter is powered by [TinaCMS](https://app.tina.io) for you and your team to visually live edit the structured content of your website. ✨ + +The content is managed through Markdown and JSON files stored in your GitHub repository, and queried through Tina GraphQL API. + +### Features + +- [Tina Headless CMS](https://app.tina.io) for authentication, content modeling, visual editing and team management. +- [Vercel](https://vercel.com) deployment to visually edit your site from the `/admin` route. +- Local development workflow from the filesystem with a local GraqhQL server. + +## Requirements + +- Git, [Node.js Active LTS](https://nodejs.org/en/about/releases/), Yarn installed for local development. +- A [TinaCMS](https://app.tina.io) account for live editing. + +## Local Development + +Install the project's dependencies: + +``` +yarn install +``` + +Run the project locally: + +``` +yarn dev +``` + +### Local URLs + +- http://localhost:3000 : browse the website +- http://localhost:3000/admin : connect to Tina Cloud and go in edit mode +- http://localhost:3000/exit-admin : log out of Tina Cloud +- http://localhost:4001/altair/ : GraphQL playground to test queries and browse the API documentation + +### Building the Starter Locally (Using the hosted content API) + +Replace the `.env.example`, with `.env` + +``` +NEXT_PUBLIC_TINA_CLIENT_ID= +TINA_TOKEN= +NEXT_PUBLIC_TINA_BRANCH= +``` + +Build the project: + +```bash +yarn build +``` + +## Getting Help + +To get help with any TinaCMS challenges you may have: + +- Visit the [documentation](https://tina.io/docs/) to learn about Tina. +- [Join our Discord](https://discord.gg/zumN63Ybpf) to share feedback. +- Visit the [community forum](https://community.tinacms.org/) to ask questions. +- Get support through the chat widget on the TinaCMS Dashboard +- [Email us](mailto:support@tina.io) to schedule a call with our team and share more about your context and what you're trying to achieve. +- [Search or open an issue](https://github.com/tinacms/tinacms/issues) if something is not working. +- Reach out on Twitter at [@tina_cms](https://twitter.com/tina_cms). + +## Development tips + +### Visual Studio Code GraphQL extension + +[Install the GraphQL extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) to benefit from type auto-completion. + +### Typescript + +A good way to ensure your components match the shape of your data is to leverage the auto-generated TypeScript types. +These are rebuilt when your `tina` config changes. + +## LICENSE + +Licensed under the [Apache 2.0 license](./LICENSE). diff --git a/app/[...filename]/client-page.tsx b/app/[...filename]/client-page.tsx new file mode 100644 index 0000000..e39b54e --- /dev/null +++ b/app/[...filename]/client-page.tsx @@ -0,0 +1,19 @@ +"use client"; +import { useTina } from "tinacms/dist/react"; +import { Blocks } from "../../components/blocks"; +import { PageQuery } from "../../tina/__generated__/types"; + +interface ClientPageProps { + data: { + page: PageQuery["page"]; + }; + variables: { + relativePath: string; + }; + query: string; +} + +export default function ClientPage(props: ClientPageProps) { + const { data } = useTina({...props}); + return ; +} diff --git a/app/[...filename]/page.tsx b/app/[...filename]/page.tsx new file mode 100644 index 0000000..e78dd17 --- /dev/null +++ b/app/[...filename]/page.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import client from "../../tina/__generated__/client"; +import ClientPage from "./client-page"; +import Layout from "../../components/layout/layout"; + +export default async function Page({ + params, +}: { + params: { filename: string[] }; +}) { + const data = await client.queries.page({ + relativePath: `${params.filename}.md`, + }); + + return ( + + + + ); +} + +export async function generateStaticParams() { + const pages = await client.queries.pageConnection(); + const paths = pages.data?.pageConnection.edges.map((edge) => ({ + filename: edge.node._sys.breadcrumbs, + })); + + return paths || []; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..04e57e2 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,69 @@ +import "../styles.css"; +import React from "react"; +import { ThemeProvider } from "../components/theme-provider"; +import { Inter as FontSans, Lato, Nunito } from "next/font/google"; +import { cn } from "../lib/utils"; +import { Metadata } from "next"; +import client from "../tina/__generated__/client"; + +const fontSans = FontSans({ + subsets: ["latin"], + variable: "--font-sans", +}); + +const nunito = Nunito({ + subsets: ["latin"], + variable: "--font-nunito", +}); + +const lato = Lato({ + subsets: ["latin"], + variable: "--font-lato", + weight: "400", +}); + +export const metadata: Metadata = { + title: "Tina", + description: "Tina Cloud Starter", +}; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const globalQuery = await client.queries.global({ + relativePath: "index.json", + }); + const global = globalQuery.data.global; + + const selectFont = (fontName: string) => { + switch (fontName) { + case "nunito": + return `font-nunito ${nunito.variable}`; + case "lato": + return `font-lato ${lato.variable}`; + case "sans": + default: + return `font-sans ${fontSans.variable} `; + } + }; + const fontVariable = selectFont(global.theme.font); + + return ( + + + + {children} + + + + ); +} diff --git a/app/posts/[...filename]/client-page.tsx b/app/posts/[...filename]/client-page.tsx new file mode 100644 index 0000000..1051c7c --- /dev/null +++ b/app/posts/[...filename]/client-page.tsx @@ -0,0 +1,130 @@ +"use client"; +import React from "react"; +import Image from "next/image"; +import { useLayout } from "../../../components/layout/layout-context"; +import { Section } from "../../../components/layout/section"; +import { Container } from "../../../components/layout/container"; +import { tinaField, useTina } from "tinacms/dist/react"; +import { format } from "date-fns"; +import { PostQuery } from "../../../tina/__generated__/types"; +import { TinaMarkdown } from "tinacms/dist/rich-text"; +import { components } from "../../../components/mdx-components"; + +const titleColorClasses = { + blue: "from-blue-400 to-blue-600 dark:from-blue-300 dark:to-blue-500", + teal: "from-teal-400 to-teal-600 dark:from-teal-300 dark:to-teal-500", + green: "from-green-400 to-green-600", + red: "from-red-400 to-red-600", + pink: "from-pink-300 to-pink-500", + purple: + "from-purple-400 to-purple-600 dark:from-purple-300 dark:to-purple-500", + orange: + "from-orange-300 to-orange-600 dark:from-orange-200 dark:to-orange-500", + yellow: + "from-yellow-400 to-yellow-500 dark:from-yellow-300 dark:to-yellow-500", +}; + +interface ClientPostProps { + data: PostQuery; + variables: { + relativePath: string; + }; + query: string; +} + +export default function PostClientPage(props: ClientPostProps) { + const { theme } = useLayout(); + const { data } = useTina({ ...props }); + const post = data.post; + + const date = new Date(post.date); + let formattedDate = ""; + if (!isNaN(date.getTime())) { + formattedDate = format(date, "MMM dd, yyyy"); + } + + return ( +
+ +

+ + {post.title} + +

+
+ {post.author && ( + <> +
+ {post.author.name} +
+

+ {post.author.name} +

+ + — + + + )} +

+ {formattedDate} +

+
+
+ {post.heroImg && ( +
+
+ + {post.title} +
+
+ )} + +
+ +
+
+
+ ); +} diff --git a/app/posts/[...filename]/page.tsx b/app/posts/[...filename]/page.tsx new file mode 100644 index 0000000..6042df8 --- /dev/null +++ b/app/posts/[...filename]/page.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import client from "../../../tina/__generated__/client"; +import Layout from "../../../components/layout/layout"; +import PostClientPage from "./client-page"; + +export default async function PostPage({ + params, +}: { + params: { filename: string[] }; +}) { + const data = await client.queries.post({ + relativePath: `${params.filename.join("/")}.mdx`, + }); + + return ( + + + + ); +} + +export async function generateStaticParams() { + const posts = await client.queries.postConnection(); + const paths = posts.data?.postConnection.edges.map((edge) => ({ + filename: edge.node._sys.breadcrumbs, + })); + return paths || []; +} diff --git a/app/posts/client-page.tsx b/app/posts/client-page.tsx new file mode 100644 index 0000000..a2e0bf7 --- /dev/null +++ b/app/posts/client-page.tsx @@ -0,0 +1,92 @@ +"use client"; +import { format } from "date-fns"; +import Link from "next/link"; +import Image from "next/image"; +import React from "react"; +import { useLayout } from "../../components/layout/layout-context"; +import { BsArrowRight } from "react-icons/bs"; +import { TinaMarkdown } from "tinacms/dist/rich-text"; +import { + PostConnectionQuery, + PostConnectionQueryVariables, +} from "../../tina/__generated__/types"; +import { useTina } from "tinacms/dist/react"; + +const titleColorClasses = { + blue: "group-hover:text-blue-600 dark:group-hover:text-blue-300", + teal: "group-hover:text-teal-600 dark:group-hover:text-teal-300", + green: "group-hover:text-green-600 dark:group-hover:text-green-300", + red: "group-hover:text-red-600 dark:group-hover:text-red-300", + pink: "group-hover:text-pink-600 dark:group-hover:text-pink-300", + purple: "group-hover:text-purple-600 dark:group-hover:text-purple-300", + orange: "group-hover:text-orange-600 dark:group-hover:text-orange-300", + yellow: "group-hover:text-yellow-500 dark:group-hover:text-yellow-300", +}; +interface ClientPostProps { + data: PostConnectionQuery; + variables: PostConnectionQueryVariables; + query: string; +} + +export default function PostsClientPage(props: ClientPostProps) { + const { data } = useTina({ ...props }); + const { theme } = useLayout(); + + return ( + <> + {data?.postConnection.edges.map((postData) => { + const post = postData.node; + const date = new Date(post.date); + let formattedDate = ""; + if (!isNaN(date.getTime())) { + formattedDate = format(date, "MMM dd, yyyy"); + } + return ( + +

+ {post.title}{" "} + + + +

+
+ +
+
+
+ {post?.author?.name} +
+

+ {post?.author?.name} +

+ {formattedDate !== "" && ( + <> + + — + +

+ {formattedDate} +

+ + )} +
+ + ); + })} + + ); +} diff --git a/app/posts/page.tsx b/app/posts/page.tsx new file mode 100644 index 0000000..45d9e2c --- /dev/null +++ b/app/posts/page.tsx @@ -0,0 +1,17 @@ +import Layout from "../../components/layout/layout"; +import client from "../../tina/__generated__/client"; +import PostsClientPage from "./client-page"; + +export default async function PostsPage() { + const posts = await client.queries.postConnection(); + + if (!posts) { + return null; + } + + return ( + + + + ); +} diff --git a/assets/img/scores/lighthouse.svg b/assets/img/scores/lighthouse.svg new file mode 100644 index 0000000..8b661f2 --- /dev/null +++ b/assets/img/scores/lighthouse.svg @@ -0,0 +1 @@ +lighthouse: 88%lighthouse88% \ No newline at end of file diff --git a/assets/img/scores/lighthouse_accessibility.svg b/assets/img/scores/lighthouse_accessibility.svg new file mode 100644 index 0000000..f869c8d --- /dev/null +++ b/assets/img/scores/lighthouse_accessibility.svg @@ -0,0 +1 @@ +lighthouse accessibility: 100%lighthouse accessibility100% \ No newline at end of file diff --git a/assets/img/scores/lighthouse_best-practices.svg b/assets/img/scores/lighthouse_best-practices.svg new file mode 100644 index 0000000..a65fba4 --- /dev/null +++ b/assets/img/scores/lighthouse_best-practices.svg @@ -0,0 +1 @@ +lighthouse best-practices: 100%lighthouse best-practices100% \ No newline at end of file diff --git a/assets/img/scores/lighthouse_performance.svg b/assets/img/scores/lighthouse_performance.svg new file mode 100644 index 0000000..1268e55 --- /dev/null +++ b/assets/img/scores/lighthouse_performance.svg @@ -0,0 +1 @@ +lighthouse performance: 100%lighthouse performance100% \ No newline at end of file diff --git a/assets/img/scores/lighthouse_pwa.svg b/assets/img/scores/lighthouse_pwa.svg new file mode 100644 index 0000000..65b27c4 --- /dev/null +++ b/assets/img/scores/lighthouse_pwa.svg @@ -0,0 +1 @@ +lighthouse pwa: 42%lighthouse pwa42% \ No newline at end of file diff --git a/assets/img/scores/lighthouse_seo.svg b/assets/img/scores/lighthouse_seo.svg new file mode 100644 index 0000000..4d30d4e --- /dev/null +++ b/assets/img/scores/lighthouse_seo.svg @@ -0,0 +1 @@ +lighthouse seo: 100%lighthouse seo100% \ No newline at end of file diff --git a/components/blocks/actions.tsx b/components/blocks/actions.tsx new file mode 100644 index 0000000..17664df --- /dev/null +++ b/components/blocks/actions.tsx @@ -0,0 +1,116 @@ +"use client"; +import Link from "next/link"; +import * as React from "react"; +import { BiRightArrowAlt } from "react-icons/bi"; +import { PageBlocksHeroActions } from "../../tina/__generated__/types"; +import { tinaField } from "tinacms/dist/react"; +import { useLayout } from "../layout/layout-context"; + +const buttonColorClasses = { + blue: "text-white bg-blue-500 hover:bg-blue-600 bg-gradient-to-r from-blue-400 to-blue-600 hover:from-blue-400 hover:to-blue-500", + teal: "text-white bg-teal-500 hover:bg-teal-600 bg-gradient-to-r from-teal-400 to-teal-600 hover:from-teal-400 hover:to-teal-500", + green: + "text-white bg-green-500 hover:bg-green-600 bg-gradient-to-r from-green-400 to-green-600 hover:from-green-400 hover:to-green-500", + red: "text-white bg-red-500 hover:bg-red-600 bg-gradient-to-r from-red-500 to-red-600 hover:from-red-400 hover:to-red-500", + pink: "text-white bg-pink-500 hover:bg-pink-600 bg-gradient-to-r from-pink-400 to-pink-600 hover:from-pink-400 hover:to-pink-500", + purple: + "text-white bg-purple-500 hover:bg-purple-600 bg-gradient-to-r from-purple-400 to-purple-600 hover:from-purple-400 hover:to-purple-500", + orange: + "text-white bg-orange-500 hover:bg-orange-600 bg-gradient-to-r from-orange-400 to-orange-600 hover:from-orange-400 hover:to-orange-500", + yellow: + "text-gray-800 bg-yellow-500 hover:bg-yellow-600 bg-gradient-to-r from-yellow-400 to-yellow-600 hover:from-yellow-400 hover:to-yellow-500", +}; + +const invertedButtonColorClasses = { + blue: "text-blue-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + teal: "text-teal-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + green: + "text-green-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + red: "text-red-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + pink: "text-pink-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + purple: + "text-purple-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + orange: + "text-orange-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", + yellow: + "text-yellow-500 bg-white hover:bg-gray-50 bg-gradient-to-r from-gray-50 to-white hover:to-gray-100", +}; + +const linkButtonColorClasses = { + blue: "text-blue-600 dark:text-blue-400 hover:text-blue-400 dark:hover:text-blue-200", + teal: "ttext-teal-600 dark:text-teal-400 hover:text-teal-400 dark:hover:text-teal-200", + green: + "text-green-600 dark:text-green-400 hover:text-green-400 dark:hover:text-green-200", + red: "text-red-600 dark:text-red-400 hover:text-red-400 dark:hover:text-red-200", + pink: "text-pink-600 dark:text-pink-400 hover:text-pink-400 dark:hover:text-pink-200", + purple: + "text-purple-600 dark:text-purple-400 hover:text-purple-400 dark:hover:text-purple-200", + orange: + "text-orange-600 dark:text-orange-400 hover:text-orange-400 dark:hover:text-orange-200", + yellow: + "text-yellow-600 dark:text-yellow-400 hover:text-yellow-400 dark:hover:text-yellow-200", +}; + +export const Actions = ({ + parentColor = "default", + className = "", + actions, +}: { + parentColor: string; + className: string; + actions: PageBlocksHeroActions[]; +}) => { + const { theme } = useLayout(); + return ( +
+ {actions && + actions.map(function (action, index) { + let element = null; + if (action.type === "button") { + element = ( + + + + ); + } + if (action.type === "link" || action.type === "linkExternal") { + element = ( + + {action.label} + {action.icon && ( + + )} + + ); + } + return element; + })} +
+ ); +}; diff --git a/components/blocks/content.tsx b/components/blocks/content.tsx new file mode 100644 index 0000000..364b6ed --- /dev/null +++ b/components/blocks/content.tsx @@ -0,0 +1,54 @@ +"use client"; +import React from "react"; + +import { TinaMarkdown } from "tinacms/dist/rich-text"; +import type { Template } from "tinacms"; +import { PageBlocksContent } from "../../tina/__generated__/types"; +import { tinaField } from "tinacms/dist/react"; +import { Container } from "../layout/container"; +import { Section } from "../layout/section"; + +export const Content = ({ data }: { data: PageBlocksContent }) => { + return ( +
+ + + +
+ ); +}; + +export const contentBlockSchema: Template = { + name: "content", + label: "Content", + ui: { + previewSrc: "/blocks/content.png", + defaultItem: { + body: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec odio. Quisque volutpat mattis eros. Nullam malesuada erat ut turpis. Suspendisse urna nibh, viverra non, semper suscipit, posuere a, pede.", + }, + }, + fields: [ + { + type: "rich-text", + label: "Body", + name: "body", + }, + { + type: "string", + label: "Color", + name: "color", + options: [ + { label: "Default", value: "default" }, + { label: "Tint", value: "tint" }, + { label: "Primary", value: "primary" }, + ], + }, + ], +}; diff --git a/components/blocks/features.tsx b/components/blocks/features.tsx new file mode 100644 index 0000000..64912c2 --- /dev/null +++ b/components/blocks/features.tsx @@ -0,0 +1,131 @@ +"use client"; +import { + PageBlocksFeatures, + PageBlocksFeaturesItems, +} from "../../tina/__generated__/types"; +import { tinaField } from "tinacms/dist/react"; +import { Icon } from "../icon"; +import { Section } from "../layout/section"; +import { Container } from "../layout/container"; +import { iconSchema } from "../../tina/fields/icon"; + +export const Feature = ({ + featuresColor, + data, +}: { + featuresColor: string; + data: PageBlocksFeaturesItems; +}) => { + return ( +
+ {data.icon && ( + + )} + {data.title && ( +

+ {data.title} +

+ )} + {data.text && ( +

+ {data.text} +

+ )} +
+ ); +}; + +export const Features = ({ data }: { data: PageBlocksFeatures }) => { + return ( +
+ + {data.items && + data.items.map(function (block, i) { + return ; + })} + +
+ ); +}; + +const defaultFeature = { + title: "Here's Another Feature", + text: "This is where you might talk about the feature, if this wasn't just filler text.", + icon: { + color: "", + style: "float", + name: "", + }, +}; + +export const featureBlockSchema = { + name: "features", + label: "Features", + ui: { + previewSrc: "/blocks/features.png", + defaultItem: { + items: [defaultFeature, defaultFeature, defaultFeature], + }, + }, + fields: [ + { + type: "object", + label: "Feature Items", + name: "items", + list: true, + ui: { + itemProps: (item) => { + return { + label: item?.title, + }; + }, + defaultItem: { + ...defaultFeature, + }, + }, + fields: [ + iconSchema, + { + type: "string", + label: "Title", + name: "title", + }, + { + type: "string", + label: "Text", + name: "text", + ui: { + component: "textarea", + }, + }, + ], + }, + { + type: "string", + label: "Color", + name: "color", + options: [ + { label: "Default", value: "default" }, + { label: "Tint", value: "tint" }, + { label: "Primary", value: "primary" }, + ], + }, + ], +}; diff --git a/components/blocks/hero.tsx b/components/blocks/hero.tsx new file mode 100644 index 0000000..745948f --- /dev/null +++ b/components/blocks/hero.tsx @@ -0,0 +1,213 @@ +"use client"; +import * as React from "react"; +import { TinaMarkdown } from "tinacms/dist/rich-text"; +import type { Template } from "tinacms"; +import { PageBlocksHero } from "../../tina/__generated__/types"; +import { tinaField } from "tinacms/dist/react"; +import Image from "next/image"; +import { Section } from "../layout/section"; +import { Container } from "../layout/container"; +import { Actions } from "./actions"; + +export const Hero = ({ data }: { data: PageBlocksHero }) => { + const headlineColorClasses = { + blue: "from-blue-400 to-blue-600", + teal: "from-teal-400 to-teal-600", + green: "from-green-400 to-green-600", + red: "from-red-400 to-red-600", + pink: "from-pink-400 to-pink-600", + purple: "from-purple-400 to-purple-600", + orange: "from-orange-300 to-orange-600", + yellow: "from-yellow-400 to-yellow-600", + }; + + return ( +
+ +
+ {data.tagline && ( +

+ {data.tagline} + +

+ )} + {data.headline && ( +

+ + {data.headline} + +

+ )} +
+
+ {data.text && ( +
+ +
+ )} +
+ {data.image && ( +
+ {data.image.alt} +
+ )} +
+ {data.text2 && ( +
+ +
+ )} + {data.actions && ( +
+ +
+ )} +
+
+
+ ); +}; + +export const heroBlockSchema: Template = { + name: "hero", + label: "Hero", + ui: { + previewSrc: "/blocks/hero.png", + defaultItem: { + tagline: "Here's some text above the other text", + headline: "This Big Text is Totally Awesome", + text: "Phasellus scelerisque, libero eu finibus rutrum, risus risus accumsan libero, nec molestie urna dui a leo.", + }, + }, + fields: [ + { + type: "string", + label: "Tagline", + name: "tagline", + }, + { + type: "string", + label: "Headline", + name: "headline", + }, + { + label: "Text-1", + name: "text", + type: "rich-text", + }, + { + type: "rich-text", + label: "Text-2", + name: "text2", + }, + { + label: "Actions", + name: "actions", + type: "object", + list: true, + ui: { + defaultItem: { + label: "Action Label", + type: "button", + icon: true, + link: "/", + }, + itemProps: (item) => ({ label: item.label }), + }, + fields: [ + { + label: "Label", + name: "label", + type: "string", + }, + { + label: "Type", + name: "type", + type: "string", + options: [ + { label: "Button", value: "button" }, + { label: "Link", value: "link" }, + ], + }, + { + label: "Icon", + name: "icon", + type: "boolean", + }, + { + label: "Link", + name: "link", + type: "string", + }, + ], + }, + { + type: "object", + label: "Image", + name: "image", + fields: [ + { + name: "src", + label: "Image Source", + type: "image", + }, + { + name: "alt", + label: "Alt Text", + type: "string", + }, + ], + }, + { + type: "string", + label: "Color", + name: "color", + options: [ + { label: "Default", value: "default" }, + { label: "Tint", value: "tint" }, + { label: "Primary", value: "primary" }, + ], + }, + ], +}; diff --git a/components/blocks/index.tsx b/components/blocks/index.tsx new file mode 100644 index 0000000..cc21ab7 --- /dev/null +++ b/components/blocks/index.tsx @@ -0,0 +1,37 @@ +import { tinaField } from "tinacms/dist/react"; +import { Page, PageBlocks } from "../../tina/__generated__/types"; +import { Hero } from "./hero"; +import { Content } from "./content"; +import { Features } from "./features"; +import { Testimonial } from "./testimonial"; + +export const Blocks = (props: Omit) => { + return ( + <> + {props.blocks + ? props.blocks.map(function (block, i) { + return ( +
+ +
+ ); + }) + : null} + + ); +}; + +const Block = (block: PageBlocks) => { + switch (block.__typename) { + case "PageBlocksHero": + return ; + case "PageBlocksContent": + return ; + case "PageBlocksFeatures": + return ; + case "PageBlocksTestimonial": + return ; + default: + return null; + } +}; diff --git a/components/blocks/testimonial.tsx b/components/blocks/testimonial.tsx new file mode 100644 index 0000000..a0376b8 --- /dev/null +++ b/components/blocks/testimonial.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import type { Template } from "tinacms"; +import { PageBlocksTestimonial } from "../../tina/__generated__/types"; +import { tinaField } from "tinacms/dist/react"; +import { Section } from "../layout/section"; +import { Container } from "../layout/container"; + +export const Testimonial = ({ data }: { data: PageBlocksTestimonial }) => { + return ( +
+ +
+
+ + “ + +

+ {data.quote} +

+ + ” + +
+
+ +
+
+

+ {data.author} +

+
+
+
+
+ ); +}; + +export const testimonialBlockSchema: Template = { + name: "testimonial", + label: "Testimonial", + ui: { + previewSrc: "/blocks/testimonial.png", + defaultItem: { + quote: + "There are only two hard things in Computer Science: cache invalidation and naming things.", + author: "Phil Karlton", + color: "primary", + }, + }, + fields: [ + { + type: "string", + ui: { + component: "textarea", + }, + label: "Quote", + name: "quote", + }, + { + type: "string", + label: "Author", + name: "author", + }, + { + type: "string", + label: "Color", + name: "color", + options: [ + { label: "Default", value: "default" }, + { label: "Tint", value: "tint" }, + { label: "Primary", value: "primary" }, + ], + }, + ], +}; diff --git a/components/icon.tsx b/components/icon.tsx new file mode 100644 index 0000000..0469d72 --- /dev/null +++ b/components/icon.tsx @@ -0,0 +1,129 @@ +"use client"; +import * as BoxIcons from "react-icons/bi"; +import React from "react"; +import { useLayout } from "./layout/layout-context"; + +export const IconOptions = { + Tina: (props) => ( + + Tina + + + + ), + ...BoxIcons, +}; + +const iconColorClass: { + [name: string]: { regular: string; circle: string }; +} = { + blue: { + regular: "text-blue-400", + circle: "bg-blue-400 dark:bg-blue-500 text-blue-50", + }, + teal: { + regular: "text-teal-400", + circle: "bg-teal-400 dark:bg-teal-500 text-teal-50", + }, + green: { + regular: "text-green-400", + circle: "bg-green-400 dark:bg-green-500 text-green-50", + }, + red: { + regular: "text-red-400", + circle: "bg-red-400 dark:bg-red-500 text-red-50", + }, + pink: { + regular: "text-pink-400", + circle: "bg-pink-400 dark:bg-pink-500 text-pink-50", + }, + purple: { + regular: "text-purple-400", + circle: "bg-purple-400 dark:bg-purple-500 text-purple-50", + }, + orange: { + regular: "text-orange-400", + circle: "bg-orange-400 dark:bg-orange-500 text-orange-50", + }, + yellow: { + regular: "text-yellow-400", + circle: "bg-yellow-400 dark:bg-yellow-500 text-yellow-50", + }, + white: { + regular: "text-white opacity-80", + circle: "bg-white-400 dark:bg-white-500 text-white-50", + }, +}; + +const iconSizeClass = { + xs: "w-6 h-6 flex-shrink-0", + small: "w-8 h-8 flex-shrink-0", + medium: "w-12 h-12 flex-shrink-0", + large: "w-14 h-14 flex-shrink-0", + xl: "w-16 h-16 flex-shrink-0", + custom: "", +}; + +export const Icon = ({ + data, + parentColor = "", + className = "", + tinaField = "", +}) => { + const { theme } = useLayout(); + + if (IconOptions[data.name] === null || IconOptions[data.name] === undefined) { + return null; + } + + const { name, color, size = "medium", style = "regular" } = data; + + const IconSVG = IconOptions[name]; + + const iconSizeClasses = + typeof size === "string" + ? iconSizeClass[size] + : iconSizeClass[Object.keys(iconSizeClass)[size]]; + + const iconColor = color + ? color === "primary" + ? theme.color + : color + : theme.color; + + if (style == "circle") { + return ( +
+ +
+ ); + } else { + const iconColorClasses = + iconColorClass[ + parentColor === "primary" && + (iconColor === theme.color || iconColor === "primary") + ? "white" + : iconColor + ].regular; + return ( + + ); + } +}; diff --git a/components/layout/container.tsx b/components/layout/container.tsx new file mode 100644 index 0000000..b3a942f --- /dev/null +++ b/components/layout/container.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { cn } from "../../lib/utils"; + +export const Container = ({ + children, + size = "medium", + width = "large", + className = "", + ...props +}) => { + const verticalPadding = { + custom: "", + small: "py-8", + medium: "py-12", + large: "py-24", + default: "py-12", + }; + const widthClass = { + small: "max-w-4xl", + medium: "max-w-5xl", + large: "max-w-7xl", + custom: "", + }; + + return ( +
+ {children} +
+ ); +}; diff --git a/components/layout/layout-context.tsx b/components/layout/layout-context.tsx new file mode 100644 index 0000000..1206442 --- /dev/null +++ b/components/layout/layout-context.tsx @@ -0,0 +1,62 @@ +"use client"; +import React, { useState, useContext } from "react"; +import { GlobalQuery } from "../../tina/__generated__/types"; + +interface LayoutState { + globalSettings: GlobalQuery["global"]; + setGlobalSettings: React.Dispatch< + React.SetStateAction + >; + pageData: {}; + setPageData: React.Dispatch>; + theme: GlobalQuery["global"]["theme"]; +} + +const LayoutContext = React.createContext(undefined); + +export const useLayout = () => { + const context = useContext(LayoutContext); + return ( + context || { + theme: { + color: "blue", + darkMode: "default", + }, + globalSettings: undefined, + pageData: undefined, + } + ); +}; + +interface LayoutProviderProps { + children: React.ReactNode; + globalSettings: GlobalQuery["global"]; + pageData: {}; +} + +export const LayoutProvider: React.FC = ({ + children, + globalSettings: initialGlobalSettings, + pageData: initialPageData, +}) => { + const [globalSettings, setGlobalSettings] = useState( + initialGlobalSettings + ); + const [pageData, setPageData] = useState<{}>(initialPageData); + + const theme = globalSettings.theme; + + return ( + + {children} + + ); +}; diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx new file mode 100644 index 0000000..e31ff25 --- /dev/null +++ b/components/layout/layout.tsx @@ -0,0 +1,30 @@ +import React, { PropsWithChildren } from "react"; +import { LayoutProvider } from "./layout-context"; +import client from "../../tina/__generated__/client"; +import Header from "../nav/header"; +import Footer from "../nav/footer"; +import { cn } from "../../lib/utils"; + +type LayoutProps = PropsWithChildren & { + rawPageData?: any; +}; + +export default async function Layout({ children, rawPageData }: LayoutProps) { + const { data: globalData } = await client.queries.global({ + relativePath: "index.json", + }); + + return ( + +
+
+ {children} +
+