diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c87c9b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md index 8f12c11..c5ec5c5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,26 @@ # hlilo-website +## Par où commencer + +```bash +yarn dev +``` + +Ouvrir [http://localhost:3000](http://localhost:3000). + +## Voir aussi + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## En savoir plus + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! diff --git a/config.json b/config.json new file mode 100644 index 0000000..708c99e --- /dev/null +++ b/config.json @@ -0,0 +1,19 @@ +{ + "base_url": "https://example.com", + "site_title": "Next.js Static CMS Blog", + "site_description": "example.com", + "site_keywords": [ + { + "keyword": "Next.js" + }, + { + "keyword": "Static CMS" + }, + { + "keyword": "React" + } + ], + "posts_per_page": 5, + "twitter_account": "@my-account", + "github_account": "myaccount" +} \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..a843cbe --- /dev/null +++ b/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +module.exports = nextConfig diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a47f76 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "hlilo-website", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@staticcms/core": "^1.2.13", + "@types/node": "18.15.11", + "@types/react": "18.0.31", + "@types/react-dom": "18.0.11", + "eslint": "8.37.0", + "eslint-config-next": "13.2.4", + "next": "13.2.4", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "5.0.3" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/next.svg b/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..0d6e338 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,14 @@ +{ + "short_name": "", + "name": "", + "icons": [ + { + "src": "icon.png", + "type": "image/png", + "sizes": "192x192" + } + ], + "start_url": "/", + "background_color": "#000000", + "theme_color": "#000000" +} diff --git a/public/thirteen.svg b/public/thirteen.svg new file mode 100644 index 0000000..8977c1b --- /dev/null +++ b/public/thirteen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..d2f8422 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Burger.tsx b/src/components/Burger.tsx new file mode 100644 index 0000000..735caf8 --- /dev/null +++ b/src/components/Burger.tsx @@ -0,0 +1,60 @@ +type Props = { + active: boolean; + onClick: () => void; +}; +export default function Burger({ active, onClick }: Props) { + return ( +
+
+
+
+ +
+ ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..9a579f3 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,23 @@ +import Head from "next/head"; +import Navigation from "./Navigation"; + +type Props = { + children: React.ReactNode; +}; +export default function Layout({ children }: Props) { + return ( +
+ + + + + + + + +
{children}
+
+ ); +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx new file mode 100644 index 0000000..ec40c0a --- /dev/null +++ b/src/components/Navigation.tsx @@ -0,0 +1,86 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import Burger from "./Burger"; +import { useState } from "react"; + +export default function Navigation() { + const router = useRouter(); + const [active, setActive] = useState(false); + return ( + <> + setActive(!active)} /> +
+
    +
  • + + about + +
  • +
  • + + blog + +
  • +
+ +
+ + ); +} diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..1b34188 --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,46 @@ +import { generatePagination } from "../lib/pagination"; +import Link from "next/link"; + +type Props = { + current: number; + pages: number; + link: { + href: (page: number) => string; + as: (page: number) => string; + }; +}; +export default function Pagination({ current, pages, link }: Props) { + const pagination = generatePagination(current, pages); + return ( +
    + {pagination.map((it, i) => ( +
  • + {it.excerpt ? ( + "..." + ) : ( + + {it.page} + + )} +
  • + ))} + +
+ ); +} diff --git a/src/components/cms/CMSPage.tsx b/src/components/cms/CMSPage.tsx new file mode 100644 index 0000000..4e8da9e --- /dev/null +++ b/src/components/cms/CMSPage.tsx @@ -0,0 +1,39 @@ +import CMS from "@staticcms/core"; +import { useEffect } from "react"; + +import config from "./config"; + +interface PostData { + title: string; + date: string; + body: string; +} + +const CMSPage = () => { + useEffect(() => { + if (process.env.NODE_ENV === "development") { + config.local_backend = true; + } + + CMS.init({ config }); + }, []); + + return ( +
+ +
+ ); +}; + +CMSPage.displayName = "CMSPage"; + +export default CMSPage; diff --git a/src/components/cms/config.ts b/src/components/cms/config.ts new file mode 100644 index 0000000..b3a0992 --- /dev/null +++ b/src/components/cms/config.ts @@ -0,0 +1,137 @@ +import type { Config } from "@staticcms/core"; + +const config: Config = { + backend: { name: "git-gateway", branch: "static" }, + media_folder: "public/images", + public_folder: "/images", + collections: [ + { + name: "config", + label: "Config", + delete: false, + editor: { preview: false }, + files: [ + { + name: "general", + label: "Site Config", + file: "config.json", + description: "General site settings", + fields: [ + { label: "URL", name: "base_url", widget: "string", hint: "Do not enter the trailing slash of the URL" }, + { label: "Site title", name: "site_title", widget: "string" }, + { label: "Site description", name: "site_description", widget: "string" }, + { + label: "Site keywords", + name: "site_keywords", + widget: "list", + summary: "{{fields.keyword.keyword}}", + fields: [{ label: "Keyword", name: "keyword", widget: "string" }], + }, + { label: "Twitter account", name: "twitter_account", widget: "string" }, + { label: "GitHub account", name: "github_account", widget: "string" }, + ], + }, + ], + }, + { + name: "meta", + label: "Meta", + delete: false, + editor: { preview: false }, + files: [ + { + name: "authors", + label: "Authors", + file: "meta/authors.yml", + description: "Author descriptions", + fields: [ + { + name: "authors", + label: "Authors", + label_singular: "Author", + widget: "list", + fields: [ + { label: "Slug", name: "slug", widget: "string", hint: "The part of a URL identifies the author" }, + { label: "Name", name: "name", widget: "string", hint: "First and Last" }, + { label: "Introduction", name: "introduction", widget: "text" }, + ], + }, + ], + }, + { + name: "tags", + label: "Tags", + file: "meta/tags.yml", + description: "List of tags", + fields: [ + { + name: "tags", + label: "Tags", + label_singular: "Tag", + widget: "list", + fields: [ + { label: "Slug", name: "slug", widget: "string", hint: "The part of a URL identifies the tag" }, + { label: "Display Name", name: "name", widget: "string", hint: "Tag name for displaying on the site" }, + ], + }, + ], + }, + ], + }, + { + name: "posts", + label: "Posts", + folder: "content/posts/", + extension: "mdx", + format: "frontmatter", + create: true, + slug: "{{slug}}", + identifier_field: "slug", + summary: "{{title}}", + fields: [ + { label: "Slug", name: "slug", widget: "string" }, + { label: "Title", name: "title", widget: "string" }, + { + label: "Publish Date", + name: "date", + widget: "datetime", + format: "yyyy-MM-dd", + date_format: "yyyy-MM-dd", + time_format: false, + }, + { + label: "Author", + name: "author", + widget: "relation", + collection: "meta", + file: "authors", + search_fields: ["authors.*.name"], + display_fields: ["authors.*.name"], + value_field: "authors.*.slug", + }, + { + label: "Tags", + label_singular: "Tag", + name: "tags", + widget: "list", + summary: "{{fields.tag}}", + fields: [ + { + label: "Tag", + name: "tag", + widget: "relation", + collection: "meta", + file: "tags", + search_fields: ["tags.*.name"], + display_fields: ["tags.*.name"], + value_field: "tags.*.slug", + }, + ], + }, + { label: "Body", name: "body", widget: "markdown" }, + ], + }, + ], +}; + +export default config; diff --git a/src/components/meta/BasicMeta.tsx b/src/components/meta/BasicMeta.tsx new file mode 100644 index 0000000..ad0a3cb --- /dev/null +++ b/src/components/meta/BasicMeta.tsx @@ -0,0 +1,39 @@ +import Head from "next/head"; +import config from "../../lib/config"; + +type Props = { + title?: string; + description?: string; + keywords?: string[]; + author?: string; + url: string; +}; +export default function BasicMeta({ + title, + description, + keywords, + author, + url, +}: Props) { + return ( + + + {title ? [title, config.site_title].join(" | ") : config.site_title} + + + it.keyword).join(",") + } + /> + {author ? : null} + + + ); +} diff --git a/src/components/meta/JsonLdMeta.tsx b/src/components/meta/JsonLdMeta.tsx new file mode 100644 index 0000000..14b4780 --- /dev/null +++ b/src/components/meta/JsonLdMeta.tsx @@ -0,0 +1,42 @@ +import { BlogPosting } from "schema-dts"; +import { jsonLdScriptProps } from "react-schemaorg"; +import config from "../../lib/config"; +import { formatISO } from "date-fns"; +import Head from "next/head"; + +type Props = { + url: string; + title: string; + keywords?: string[]; + date: Date; + author?: string; + image?: string; + description?: string; +}; +export default function JsonLdMeta({ + url, + title, + keywords, + date, + author, + image, + description, +}: Props) { + return ( + +