diff --git a/next/blog/.eslintignore b/next/blog/.eslintignore new file mode 100644 index 0000000..6357050 --- /dev/null +++ b/next/blog/.eslintignore @@ -0,0 +1,2 @@ +/bcms.config.js +/bcms.routes.js \ No newline at end of file diff --git a/next/blog/.eslintrc.json b/next/blog/.eslintrc.json new file mode 100644 index 0000000..49a04fe --- /dev/null +++ b/next/blog/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + // "extends": "next/core-web-vitals", + "rules": { + "no-debugger": "warn", + "no-shadow": "error", + "@typescript-eslint/no-unused-vars": [ + 2, + { "args": "all", "argsIgnorePattern": "^_" } + ], + "no-unused-labels": "error", + "no-unused-expressions": "error", + "no-duplicate-imports": "error" + } +} diff --git a/next/blog/.gitignore b/next/blog/.gitignore new file mode 100644 index 0000000..03e2bb3 --- /dev/null +++ b/next/blog/.gitignore @@ -0,0 +1,44 @@ +# 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* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# BCMS +/bcms +/logs +/public/bcms-media +/public/api/bcms-images diff --git a/next/blog/.prettierrc b/next/blog/.prettierrc new file mode 100755 index 0000000..9409967 --- /dev/null +++ b/next/blog/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/next/blog/README.md b/next/blog/README.md new file mode 100644 index 0000000..2dc2c21 --- /dev/null +++ b/next/blog/README.md @@ -0,0 +1,8 @@ +# BCMS NextJS Starter + +This is a simple starter project for NextJS and [BCMS](https://thebcms.com). For more information visit [NextJS plugin repository](https://github.com/becomesco/next-plugin-bcms). + +## Getting started + +- Install dependencies: `npm i` +- Start a development server: `npm run dev` diff --git a/next/blog/api/_api-route.ts b/next/blog/api/_api-route.ts new file mode 100644 index 0000000..e6774d0 --- /dev/null +++ b/next/blog/api/_api-route.ts @@ -0,0 +1,91 @@ +import type { BCMSMost, BCMSMostServerRoute } from "@becomes/cms-most/types"; +import { HeaderEntry, HeaderEntryMeta } from "~~/bcms/types"; +import { FooterEntry, FooterEntryMeta } from "~~/bcms/types/entry/footer"; +import { APIResponse, Languages } from "~~/types"; +import {getBcmsMost} from "next-plugin-bcms"; + +interface Route + extends Omit, "handler"> { + handler(data: { + url: string; + params: { + [name: string]: string; + }; + query: { + [name: string]: string; + }; + headers: { + [name: string]: string | string[] | undefined; + }; + body: Body; + bcms: BCMSMost; + lng: Languages; + }): Promise; +} + +export function apiRoute( + route: Route +): Route< + APIResponse & { + data: Result; + }, + Body +> { + return { + method: route.method, + async handler(data) { + const lng = data.params.lng ? (data.params.lng as Languages) : "en"; + const header = (await data.bcms.content.entry.findOne( + "header", + async () => true + )) as unknown as HeaderEntry; + const footer = (await data.bcms.content.entry.findOne( + "footer", + async () => true + )) as unknown as FooterEntry; + const result = await route.handler(data); + return { + data: result, + header: header.meta[lng] as HeaderEntryMeta, + footer: footer.meta[lng] as FooterEntryMeta, + }; + }, + }; +} + +export abstract class GenericApi { + public readonly bcms: BCMSMost + + constructor() { + this.bcms = getBcmsMost() + } + + public async fetchHeaderAndFooter(): Promise<{ + header: HeaderEntryMeta; + footer: FooterEntryMeta; + }> { + const header = (await this.bcms.content.entry.findOne( + 'header', + async () => true + )) as HeaderEntry; + const footer = (await this.bcms.content.entry.findOne( + 'footer', + async () => true + )) as FooterEntry; + + return { + header: header.meta.en as HeaderEntryMeta, + footer: footer.meta.en as FooterEntryMeta, + }; + } + + + public async handler(data: T): Promise> { + const { header, footer } = await this.fetchHeaderAndFooter(); + return { + data, + header, + footer, + }; + } +} diff --git a/next/blog/api/about.ts b/next/blog/api/about.ts new file mode 100644 index 0000000..20865c1 --- /dev/null +++ b/next/blog/api/about.ts @@ -0,0 +1,33 @@ +import { BCMSPropRichTextDataParsed } from "@becomes/cms-client/types"; +import { + AboutPageEntry, + AboutPageEntryMeta, + +} from "~~/bcms/types"; +import {AboutPageData, APIResponse} from "~~/types"; +import { GenericApi} from "./_api-route"; +export class AboutApi extends GenericApi { + public async getAboutPageData (): Promise> { + try { + const entry = (await this.bcms.content.entry.findOne( + "about_page", + async () => true + )) as unknown as AboutPageEntry; + + if (!entry) { + throw new Error("About page entry does not exist."); + } + + const data = { + meta: entry.meta.en as AboutPageEntryMeta, + content: entry.content.en as BCMSPropRichTextDataParsed, + } + return await this.handler(data) + + } catch (error) { + console.error(error) + throw new Error('Failed to fetch about page data ') + } + + } +} diff --git a/next/blog/api/blogs.ts b/next/blog/api/blogs.ts new file mode 100644 index 0000000..27e4a33 --- /dev/null +++ b/next/blog/api/blogs.ts @@ -0,0 +1,90 @@ +import { BCMSPropRichTextDataParsed } from "@becomes/cms-client/types"; +import { + BlogEntry, + BlogEntryMeta, + BlogsPageEntry, + BlogsPageEntryMeta, +} from "~~/bcms/types"; +import {APIResponse, BlogLight, BlogPageData, BlogsPageData} from "~~/types"; +import {GenericApi} from "~/api/_api-route"; + +export const blogToLight = (blogs: BlogEntry[]): BlogLight[] => { + return blogs.map((e) => { + const meta = e.meta.en as BlogEntryMeta; + + return { + title: meta.title, + slug: meta.slug, + cover: meta.cover, + description: meta.description, + date: meta.date, + category: meta.category, + }; + }); +}; + +export class BlogsApi extends GenericApi { + public async getBlogs(): Promise> { + try { + const entry = (await this.bcms.content.entry.findOne( + 'blogs_page', + async () => true + )) as BlogsPageEntry; + + if (!entry) { + throw new Error('Blogs page entry does not exist.'); + } + + const blogs = (await this.bcms.content.entry.find( + 'blog', + async () => true + )) as BlogEntry[]; + + const data: BlogsPageData = { + meta: entry.meta.en as BlogsPageEntryMeta, + blogs: blogToLight( + blogs.sort((a, b) => (b.meta.en?.date || 0) - (a.meta.en?.date || 0)) + ), + }; + + return await this.handler(data); + } catch (error) { + console.error(error); + throw new Error('Cannot fetch blogs at this time. Something went wrong.'); + } + } + + public async getSingleBlog( + params: { [p: string]: string } + ): Promise> { + try { + const entry = (await this.bcms.content.entry.findOne( + 'blog', + async (e) => e.meta.en.slug === params.slug + )) as BlogEntry; + + if (!entry) { + throw new Error('Blog entry does not exist.'); + } + + const blogs = (await this.bcms.content.entry.find( + 'blog', + async (e) => e.meta.en.slug !== params.slug + )) as BlogEntry[]; + + const data: BlogPageData = { + meta: entry.meta.en as BlogEntryMeta, + content: entry.content.en as BCMSPropRichTextDataParsed, + otherBlogs: blogToLight( + blogs.sort((a, b) => (b.meta.en?.date || 0) - (a.meta.en?.date || 0)) + ).slice(0, 3), + }; + + return await this.handler(data); + } catch (error) { + console.error(error); + throw new Error('Cannot fetch blog at this time. Something went wrong.'); + } + } +} + diff --git a/next/blog/api/contact.ts b/next/blog/api/contact.ts new file mode 100644 index 0000000..05f8a9d --- /dev/null +++ b/next/blog/api/contact.ts @@ -0,0 +1,22 @@ +import { ContactPageEntry, ContactPageEntryMeta } from "~~/bcms/types"; +import {APIResponse, ContactPageData} from "~~/types"; +import { GenericApi} from "./_api-route"; + +export class ContactApi extends GenericApi { + public async getContactPage (): Promise> { + const entry = (await this.bcms.content.entry.findOne( + "contact_page", + async () => true + )) as unknown as ContactPageEntry; + + if (!entry) { + throw new Error("Contact page entry does not exist."); + } + + const data = { + meta: entry.meta.en as ContactPageEntryMeta, + }; + + return this.handler(data) + } +} diff --git a/next/blog/api/home.ts b/next/blog/api/home.ts new file mode 100644 index 0000000..33c3bff --- /dev/null +++ b/next/blog/api/home.ts @@ -0,0 +1,37 @@ +import { BlogEntry, HomePageEntry, HomePageEntryMeta } from "~~/bcms/types"; +import {APIResponse, HomePageData} from "~~/types"; +import { blogToLight } from "./blogs"; +import {GenericApi} from "./_api-route"; +export class HomeApi extends GenericApi{ + public async getHomePageData (): Promise> { + try { + const entry = (await this.bcms.content.entry.findOne( + "home_page", + async () => true + )) as unknown as HomePageEntry; + + if (!entry) { + throw new Error("Home page entry does not exist."); + } + + const blogs = (await this.bcms.content.entry.find("blog", async (e) => + entry.meta.en?.hero.featured_blogs.find( + (i) => e.meta.en?.slug !== i.meta.en?.slug + ) + )) as unknown as BlogEntry[]; + + const data = { + meta: entry.meta.en as HomePageEntryMeta, + blogs: blogToLight( + blogs.sort((a, b) => (b.meta.en?.date || 0) - (a.meta.en?.date || 0)) + ).slice(0, 6), + }; + + return await this.handler(data) + + } catch(error){ + console.error(error) + throw new Error('Failed to fetch homepage data. something went wrong') + } + } +} diff --git a/next/blog/api/index.ts b/next/blog/api/index.ts new file mode 100644 index 0000000..e64b2d2 --- /dev/null +++ b/next/blog/api/index.ts @@ -0,0 +1,4 @@ +export * from "./home"; +export * from "./blogs"; +export * from "./contact"; +export * from "./about"; diff --git a/next/blog/assets/css/main.css b/next/blog/assets/css/main.css new file mode 100644 index 0000000..a556376 --- /dev/null +++ b/next/blog/assets/css/main.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +*, +*::before, +*::after { + @apply box-border m-0 p-0; +} + +body { + @apply font-Inter overflow-x-hidden bg-appBody; +} diff --git a/next/blog/assets/css/prose.css b/next/blog/assets/css/prose.css new file mode 100644 index 0000000..0c3bba9 --- /dev/null +++ b/next/blog/assets/css/prose.css @@ -0,0 +1,12 @@ +.prose h2 { + @apply leading-none font-medium tracking-[-0.41px] mb-[14px] md:text-xl md:leading-none lg:text-[32px] lg:leading-none lg:mb-6; +} +.prose p { + @apply text-sm leading-[1.4] tracking-[-0.41px] text-appGray-500 mb-3 md:text-base md:leading-[1.4] md:mb-4 lg:text-xl lg:leading-[1.4] lg:mb-6; +} +.prose p strong { + @apply text-appText font-medium; +} +.prose p a { + @apply underline text-inherit; +} diff --git a/next/blog/assets/css/reset.css b/next/blog/assets/css/reset.css new file mode 100644 index 0000000..2fdc332 --- /dev/null +++ b/next/blog/assets/css/reset.css @@ -0,0 +1,12 @@ +.bcmsImage.cover img, +.bcmsImage.cover svg { + width: 100%; + height: 100%; + object-fit: cover; +} +.bcmsImage.position-top img { + object-position: top; +} +.bcmsImage svg { + width: 100%; +} diff --git a/next/blog/assets/css/transition.css b/next/blog/assets/css/transition.css new file mode 100644 index 0000000..6c86d13 --- /dev/null +++ b/next/blog/assets/css/transition.css @@ -0,0 +1,8 @@ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} diff --git a/next/blog/assets/icons/arrow.tsx b/next/blog/assets/icons/arrow.tsx new file mode 100644 index 0000000..a63bd3d --- /dev/null +++ b/next/blog/assets/icons/arrow.tsx @@ -0,0 +1,9 @@ +export function ArrowIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + ) +} diff --git a/next/blog/assets/icons/email.tsx b/next/blog/assets/icons/email.tsx new file mode 100644 index 0000000..fc2de7d --- /dev/null +++ b/next/blog/assets/icons/email.tsx @@ -0,0 +1,9 @@ +export function EmailIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + ) +} diff --git a/next/blog/assets/icons/mailgun.tsx b/next/blog/assets/icons/mailgun.tsx new file mode 100644 index 0000000..d20d5e6 --- /dev/null +++ b/next/blog/assets/icons/mailgun.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +export function MailGunIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + + ); +} + diff --git a/next/blog/assets/icons/menu.tsx b/next/blog/assets/icons/menu.tsx new file mode 100644 index 0000000..9be6e65 --- /dev/null +++ b/next/blog/assets/icons/menu.tsx @@ -0,0 +1,12 @@ +export function MenuIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + + + + ) +} diff --git a/next/blog/assets/icons/open.tsx b/next/blog/assets/icons/open.tsx new file mode 100644 index 0000000..d1990ef --- /dev/null +++ b/next/blog/assets/icons/open.tsx @@ -0,0 +1,23 @@ +export function OpenIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + + + + ) +} diff --git a/next/blog/assets/icons/paper-plane.tsx b/next/blog/assets/icons/paper-plane.tsx new file mode 100644 index 0000000..863deef --- /dev/null +++ b/next/blog/assets/icons/paper-plane.tsx @@ -0,0 +1,29 @@ + + +export function PaperPlaneIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + + + + + + + + ) +} diff --git a/next/blog/assets/icons/search.tsx b/next/blog/assets/icons/search.tsx new file mode 100644 index 0000000..9975091 --- /dev/null +++ b/next/blog/assets/icons/search.tsx @@ -0,0 +1,25 @@ +export function SearchIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + ) +} diff --git a/next/blog/assets/icons/x.tsx b/next/blog/assets/icons/x.tsx new file mode 100644 index 0000000..122e7bc --- /dev/null +++ b/next/blog/assets/icons/x.tsx @@ -0,0 +1,17 @@ +export function XIcon ({className}: {className: string}): JSX.Element { + return ( + + + + + ) +} diff --git a/next/blog/assets/media/email-bg.png b/next/blog/assets/media/email-bg.png new file mode 100644 index 0000000..c7b7992 Binary files /dev/null and b/next/blog/assets/media/email-bg.png differ diff --git a/next/blog/bcms-components/__v b/next/blog/bcms-components/__v new file mode 100644 index 0000000..634650e --- /dev/null +++ b/next/blog/bcms-components/__v @@ -0,0 +1 @@ +v1.1 \ No newline at end of file diff --git a/next/blog/bcms-components/content-item.tsx b/next/blog/bcms-components/content-item.tsx new file mode 100644 index 0000000..d10385f --- /dev/null +++ b/next/blog/bcms-components/content-item.tsx @@ -0,0 +1,44 @@ +import { + BCMSEntryContentNodeType, + BCMSEntryContentParsedItem, +} from '@becomes/cms-client/types'; +import type { BCMSWidgetComponents } from './content-manager'; + + +export interface BCMSContentItemI { + item: BCMSEntryContentParsedItem + components: BCMSWidgetComponents + + nodeParser?: (item: BCMSEntryContentParsedItem) => string +} + + +export function BCMSContentItem (props: BCMSContentItemI): JSX.Element { + function resolveWidget(name: string) { + if (props.components[name]) { + const Widget = props.components[name]; + return ; + } else { + return ( +
+ Widget {props.item.name} is not handled +
+ ); + } + } + return ( + <> + {props.item.name && + props.item.type === BCMSEntryContentNodeType.widget ? ( + <> + ) : ( +
+ )} + + ); +} diff --git a/next/blog/bcms-components/content-manager.tsx b/next/blog/bcms-components/content-manager.tsx new file mode 100644 index 0000000..97b555a --- /dev/null +++ b/next/blog/bcms-components/content-manager.tsx @@ -0,0 +1,65 @@ +import type { + BCMSEntryContentParsedItem, + BCMSPropRichTextDataParsed, +} from '@becomes/cms-client/types'; +import { BCMSContentItem } from './content-item'; +import classNames from "classnames"; +import {CSSProperties, RefObject} from "react"; +export interface BCMSWidgetComponents { + [name: string]: any +} + +export interface BCMSContentManagerI { + id?: string + className?: string + + style?: CSSProperties + + items: Array + + widgetComponents: BCMSWidgetComponents + + nodeParser?: (item: BCMSEntryContentParsedItem) => string + + ref?: RefObject +} + + +export function BCMSContentManager (props: BCMSContentManagerI): JSX.Element { + return ( +
+ {props.items.map((_item, _itemIdx) => { + return ( + <> + {Array.isArray(_item) ? ( + <> + {_item.map((item, itemIdx) => { + return ( + + ); + })} + + ) : ( + + )} + + ); + })} +
+ ) +} diff --git a/next/blog/bcms-components/image.tsx b/next/blog/bcms-components/image.tsx new file mode 100644 index 0000000..e36e608 --- /dev/null +++ b/next/blog/bcms-components/image.tsx @@ -0,0 +1,102 @@ +import React, { useState, useEffect, useRef, CSSProperties } from 'react'; +import { + BCMSMediaParsed, +} from '@becomes/cms-client/types' + +import {BCMSMostImageProcessorProcessOptions} from '@becomes/cms-most/types' + +import { + BCMSImageConfig, + createBcmsImageHandler, +} from '@becomes/cms-most/frontend'; + +interface BCMSImageProps { + media: BCMSMediaParsed; + className?: string; + style?: CSSProperties; + id?: string; + options?: BCMSMostImageProcessorProcessOptions; + svg?: boolean; +} + +export function BCMSImage (props: BCMSImageProps): JSX.Element{ + + const [handler, setHandler] = useState(createBcmsImageHandler(props.media, props.options)); + const container = useRef(null); + const [srcSet, setSrcSet] = useState(handler.getSrcSet()); + let mediaBuffer = JSON.stringify(props.media); + let optionsBuffer = props.options ? JSON.stringify(props.options) : ''; + + const resizeHandler = () => { + if (container.current) { + const el = container.current; + setSrcSet(handler.getSrcSet({ width: el.offsetWidth })); + } + }; + + useEffect(() => { + resizeHandler(); + window.addEventListener('resize', resizeHandler); + + return () => { + window.removeEventListener('resize', resizeHandler); + }; + }, []); + + useEffect(() => { + if ( + mediaBuffer !== JSON.stringify(props.media) || + (!props.options && optionsBuffer) || + (props.options && optionsBuffer !== JSON.stringify(props.options)) + ) { + mediaBuffer = JSON.stringify(props.media); + optionsBuffer = props.options ? JSON.stringify(props.options) : ''; + const newHandler = createBcmsImageHandler(props.media, props.options); + setHandler(newHandler); + resizeHandler(); + } + }, [props.media, props.options]); + + return ( +
+ {handler.parsable ? ( + + + + {props.media.alt_text} + + ) : props.svg && props.media.svg ? ( +
+ ) : ( + {props.media.alt_text} + )} +
+ ); +} + +export default BCMSImage; diff --git a/next/blog/bcms-components/index.ts b/next/blog/bcms-components/index.ts new file mode 100644 index 0000000..29d8e94 --- /dev/null +++ b/next/blog/bcms-components/index.ts @@ -0,0 +1,3 @@ +export * from './content-item'; +export * from './content-manager'; +export * from './image'; diff --git a/next/blog/bcms.config.js b/next/blog/bcms.config.js new file mode 100644 index 0000000..13df22c --- /dev/null +++ b/next/blog/bcms.config.js @@ -0,0 +1,20 @@ +const { createBcmsMostConfig } = require('@becomes/cms-most'); + +module.exports = createBcmsMostConfig({ + cms: { + origin: + process.env.BCMS_API_ORIGIN || + 'http://localhost:8080', + key: { + id: process.env.BCMS_API_KEY || '6433b6994c02e25452a8a947', + secret: + process.env.BCMS_API_SECRET || + '4107ecd203ff708a1789439376934e315781d66134d6c0da058bc87583f6e0c9', + }, + }, + media: { + output: 'public', + download: false, + }, + enableClientCache: true, +}); diff --git a/next/blog/components/ContentManager.tsx b/next/blog/components/ContentManager.tsx new file mode 100644 index 0000000..42c2f41 --- /dev/null +++ b/next/blog/components/ContentManager.tsx @@ -0,0 +1,38 @@ +import React, { useRef, useEffect } from 'react'; +import {BCMSContentManager, BCMSWidgetComponents} from "~/bcms-components"; +import {useRouter} from "next/router"; + +export function ContentManager ({ item, widgetComponents, className}: {item: any, widgetComponents?: BCMSWidgetComponents, className?: string}) : JSX.Element { + const router = useRouter() + const managerDOM = useRef(null); + function parseInternalLinks (): void { + if (managerDOM.current) { + const links = managerDOM?.current?.querySelectorAll('a'); + + links.forEach((link ) => { + const href = link.getAttribute('href'); + + if (href && href.startsWith('/')) { + link.target = '_self'; + link.addEventListener('click', event => { + event.preventDefault(); + void router.push(href) + }); + } + }); + } + } + + useEffect(() => { + parseInternalLinks(); + }, []); + + return ( + + ); +} diff --git a/next/blog/components/PageWrapper.tsx b/next/blog/components/PageWrapper.tsx new file mode 100644 index 0000000..c80803b --- /dev/null +++ b/next/blog/components/PageWrapper.tsx @@ -0,0 +1,19 @@ + +import {Header} from './layout/Header'; +import {Footer} from './layout/Footer'; +import { HeaderEntryMeta, FooterEntryMeta } from '~~/bcms/types'; +import {PropsWithChildren} from "react"; + +interface PageWrapperI { + header: HeaderEntryMeta; + footer: FooterEntryMeta; +} +export function PageWrapper ({header, children, footer}: PropsWithChildren): JSX.Element { + return ( +
+
+
{children}
+
+
+ ) +} diff --git a/next/blog/components/Search.tsx b/next/blog/components/Search.tsx new file mode 100644 index 0000000..16675d8 --- /dev/null +++ b/next/blog/components/Search.tsx @@ -0,0 +1,42 @@ +import {SearchIcon} from '@/assets/icons/search'; +import classNames from "classnames"; + +interface SearchProps { + value: string; + onEnter: () => void; + onChange: (value: string) => void; + + className?: string +} + +export function Search ({ value, onEnter, onChange, className }:SearchProps): JSX.Element { + const handleInput = (e: React.ChangeEvent) => { + onChange(e.target.value); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onEnter(); + }; + + return ( +
+ + + + ); +} + diff --git a/next/blog/components/TopGradient.tsx b/next/blog/components/TopGradient.tsx new file mode 100644 index 0000000..6ad9206 --- /dev/null +++ b/next/blog/components/TopGradient.tsx @@ -0,0 +1,12 @@ +import classNames from "classnames"; + +export function TopGradient (props: {className?: string}): JSX.Element { + return ( +
+ ) +} diff --git a/next/blog/components/blogs/Card.tsx b/next/blog/components/blogs/Card.tsx new file mode 100644 index 0000000..0564dfb --- /dev/null +++ b/next/blog/components/blogs/Card.tsx @@ -0,0 +1,49 @@ +import { BlogLight } from '~~/types'; +import {BCMSImage} from '~~/bcms-components'; +import {OpenIcon} from '@/assets/icons/open'; +import {ContentManager} from "~/components/ContentManager"; +import {dateUtil} from '~/utils/date' +import NextLink from "next/link"; +interface BlogsCardProps { + blog: BlogLight; +} + +export function BlogsCard ({ blog }: BlogsCardProps): JSX.Element { + return ( + + ); +} diff --git a/next/blog/components/content-page/Form.tsx b/next/blog/components/content-page/Form.tsx new file mode 100644 index 0000000..7418f6b --- /dev/null +++ b/next/blog/components/content-page/Form.tsx @@ -0,0 +1,99 @@ +import {useState} from "react"; +import {EmailIcon} from "@/assets/icons/email"; +import {FormText} from "~/components/form/Text"; +import NextImage from 'next/image' +import EmailImage from '@/assets/media/email-bg.png' + +export interface ContactFormI { + email: string +} + +export interface FormFieldsI { + email: string + name: string + question: string +} + +export function ContactPageForm(props: ContactFormI): JSX.Element { + const [form, setForm] = useState({ + name: '', + email: '', + question: '' + }) + + const handleFormChange = (value: string, field: keyof FormFieldsI) => { + setForm((prev) => ({...prev, [field]: value})) + } + + const handleSubmit = (): void => { + //Todo + } + return ( +
+
+
+
+ + + + + + +
+
+
+ + ) +} diff --git a/next/blog/components/form/Text.tsx b/next/blog/components/form/Text.tsx new file mode 100644 index 0000000..2947d21 --- /dev/null +++ b/next/blog/components/form/Text.tsx @@ -0,0 +1,64 @@ +import classNames from "classnames"; +import {FormFieldsI} from "~/components/content-page/Form"; + +type TextInputType = "text" | "email" | "textarea" + +export interface TextI { + label?: string + value: string + placeholder?: string + + type?: TextInputType + + error?: string + + name: string + + onChange: (value: string, field: keyof FormFieldsI) => void +} + +export function FormText(props: TextI): JSX.Element { + + const handleInputChange = (e: any): void => { + const element = e?.target as HTMLInputElement + props.onChange(element.value, element.name as any ) + } + return ( +