diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/.DS_Store differ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore index 65fb93e2..f5e02ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,114 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# Created by https://www.toptal.com/developers/gitignore/api/node -# Edit at https://www.toptal.com/developers/gitignore?templates=node +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov +# testing +/coverage -# nyc test coverage -.nyc_output +# next.js +/.next/ +/out/ -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt +# production +/build -# Bower dependency directory (https://bower.io/) -bower_components +# misc +.DS_Store +*.pem -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* -# Dependency directories -node_modules/ -jspm_packages/ +# local env files +.env*.local -# TypeScript v1 declaration files -typings/ +# vercel +.vercel -# TypeScript cache +# typescript *.tsbuildinfo +next-env.d.ts -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test +# amplify +.amplify +amplify_outputs* +amplifyconfiguration* -# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file +/cdk.out \ No newline at end of file diff --git a/Readme.md b/Readme.md index 4157c472..69a2852f 100644 --- a/Readme.md +++ b/Readme.md @@ -5,7 +5,8 @@ This is a community driven project for the Cloud Development Kit (CDK) and will - [AWS CDK](https://aws.amazon.com/cdk/) - [CDK for Kubernetes](https://cdk8s.io) - [Terraform CDK](https://cdk.tf) +- [Projen](https://projen.io/) ## Contributions -We'd love to have more people on board - Just start to create issues or get in involved here: https://github.com/cdk-dev/base \ No newline at end of file +We'd love to have more people on board - Head over to [cdk.dev](https://cdk.dev) and join our Slack. diff --git a/amplify/auth/resource.ts b/amplify/auth/resource.ts new file mode 100644 index 00000000..4d869d3f --- /dev/null +++ b/amplify/auth/resource.ts @@ -0,0 +1,12 @@ +import { defineAuth } from '@aws-amplify/backend'; + +/** + * Define and configure your auth resource + * @see https://docs.amplify.aws/gen2/build-a-backend/auth + */ +export const auth = defineAuth({ + loginWith: { + email: true, + }, + groups: ['admin', 'user'], +}); diff --git a/amplify/backend.ts b/amplify/backend.ts new file mode 100644 index 00000000..bad8d81a --- /dev/null +++ b/amplify/backend.ts @@ -0,0 +1,75 @@ +import { defineBackend } from '@aws-amplify/backend'; +import { auth } from './auth/resource'; +import { data } from './data/resource'; +import { storage } from './storage/resource'; +import { TableNotifications } from './constructs/table-notifications'; +import * as sns from 'aws-cdk-lib/aws-sns'; +import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; +import * as iam from 'aws-cdk-lib/aws-iam'; + +/** + * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more + */ +const backend = defineBackend({ + auth, + data, + storage +}); + + +const dataResources = backend.data.resources; + +dataResources.cfnResources.cfnGraphqlApi.xrayEnabled = true; +// Object.values(dataResources.cfnResources.amplifyDynamoDbTables).forEach((table) => { +// table.pointInTimeRecoveryEnabled = true; +// }); + + +// === Subscribers === + +const subscribers = backend.createStack('subscribers') +const topic = new sns.Topic(subscribers, 'DdbToSnsTopic', { + displayName: 'New Post on cdk.dev', +}); + +new TableNotifications(subscribers, 'TableNotifications', { + table: dataResources.tables['Post'], + topic, + message: 'A new post with the title "<$.dynamodb.NewImage.title.S>" has been added to https://cdk.dev - check it out now' +}) + +const policy = new iam.Policy(subscribers, 'SubscribersPolicy', { + document: new iam.PolicyDocument({ + statements: [new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [topic.topicArn], + actions: ['sns:Subscribe'], + })] + }), +}); + +backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(policy) +backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(policy); + +backend.addOutput({ + custom: { + subscribersTopicArn: topic.topicArn, + } +}) + +// === Link Notifications === + +const links = backend.createStack('links') +const t = new sns.Topic(links, 'DdbToSnsTopic', { + displayName: 'New Link for cdk.dev', +}); + +t.addSubscription(new snsSubscriptions.EmailSubscription('sebastian@korfmann.net')); + +new TableNotifications(links, 'TableNotifications', { + table: dataResources.tables['LinkSuggestion'], + topic: t, + message: 'A new link with the url "<$.dynamodb.NewImage.url.S>" has been added to https://cdk.dev - check it out now' +}) + +export default backend; \ No newline at end of file diff --git a/amplify/constructs/table-notifications.ts b/amplify/constructs/table-notifications.ts new file mode 100644 index 00000000..e59c5fb0 --- /dev/null +++ b/amplify/constructs/table-notifications.ts @@ -0,0 +1,46 @@ +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as sns from 'aws-cdk-lib/aws-sns'; +import * as pipes from 'aws-cdk-lib/aws-pipes'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; + +export class TableNotifications extends Construct { + constructor(scope: Construct, id: string, props: { table: dynamodb.ITable, topic: sns.ITopic, message: string }) { + super(scope, id); + + const { table, topic, message } = props; + + const pipeRole = new iam.Role(this, 'PipeRole', { + assumedBy: new iam.ServicePrincipal('pipes.amazonaws.com'), + }); + + table.grantStreamRead(pipeRole); + topic.grantPublish(pipeRole); + + // Create a CloudWatch Log Group + const logGroup = new logs.LogGroup(this, 'PipeLogGroup', { + retention: logs.RetentionDays.ONE_WEEK, + }); + + new pipes.CfnPipe(this, 'DdbToSnsPipe', { + roleArn: pipeRole.roleArn, + source: table.tableStreamArn!, + sourceParameters: { + dynamoDbStreamParameters: { + startingPosition: 'LATEST', + }, + }, + target: topic.topicArn, + targetParameters: { + inputTemplate: message, + }, + logConfiguration: { + cloudwatchLogsLogDestination: { + logGroupArn: logGroup.logGroupArn, + }, + level: 'INFO', + }, + }); + } +} diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts new file mode 100644 index 00000000..82795ed0 --- /dev/null +++ b/amplify/data/resource.ts @@ -0,0 +1,89 @@ +import { type ClientSchema, a, defineData, defineFunction } from '@aws-amplify/backend'; + +const schema = a.schema({ + Author: a + .model({ + name: a.string().required(), + avatar: a.string().required(), + posts: a.hasMany('Post', 'authorId'), + }) + .authorization((allow) => [ + allow.guest().to(['read']), + allow.authenticated('identityPool').to(['read']), + ]), + Post: a + .model({ + title: a.string().required(), + content: a.string().required(), + url: a.string().required(), + categories: a.string().required().array(), + authorId: a.id().required(), + banner: a.string(), + author: a.belongsTo('Author', 'authorId'), + }) + .authorization((allow) => [ + allow.guest().to(['read']), + allow.authenticated('identityPool').to(['read']), + ]), + Resource: a + .model({ + title: a.string().required(), + content: a.string().required(), + url: a.string().required(), + categories: a.string().required().array(), + banner: a.string() + }) + .authorization((allow) => [ + allow.guest().to(['read']), + allow.authenticated('identityPool').to(['read']), + ]), + LinkSuggestion: a + .model({ + url: a.string().required(), + comment: a.string(), + }) + .authorization((allow) => [ + allow.guest().to(['create']), + allow.authenticated('identityPool').to(['create']), + ]) +}); + +export type Schema = ClientSchema; + +export const data = defineData({ + schema, + authorizationModes: { + defaultAuthorizationMode: 'iam' + } +}); + + +/*== STEP 2 =============================================================== +Go to your frontend source code. From your client-side code, generate a +Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY +WORK IN THE FRONTEND CODE FILE.) + +Using JavaScript or Next.js React Server Components, Middleware, Server +Actions or Pages Router? Review how to generate Data clients for those use +cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/ +=========================================================================*/ + +/* +"use client" +import { generateClient } from "aws-amplify/data"; +import type { Schema } from "@/amplify/data/resource"; + +const client = generateClient() // use this Data client for CRUDL requests +*/ + +/*== STEP 3 =============================================================== +Fetch records from the database and use them in your frontend component. +(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) +=========================================================================*/ + +/* For example, in a React component, you can use this snippet in your + function's RETURN statement */ +// const { data: posts } = await client.models.Post.list() + +// return + diff --git a/amplify/package.json b/amplify/package.json new file mode 100644 index 00000000..aead43de --- /dev/null +++ b/amplify/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/amplify/storage/resource.ts b/amplify/storage/resource.ts new file mode 100644 index 00000000..b0afcedb --- /dev/null +++ b/amplify/storage/resource.ts @@ -0,0 +1,10 @@ +import { defineStorage } from '@aws-amplify/backend'; + +export const storage = defineStorage({ + name: 'cdk-dev-assets', + access: (allow) => ({ + 'content/*': [ + allow.guest.to(['read']) + ] + }), +}); \ No newline at end of file diff --git a/amplify/tsconfig.json b/amplify/tsconfig.json new file mode 100644 index 00000000..4eb4ab26 --- /dev/null +++ b/amplify/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "paths": { + "$amplify/*": [ + "../.amplify/generated/*" + ] + } + } +} \ No newline at end of file diff --git a/app/(loggedin)/layout.tsx b/app/(loggedin)/layout.tsx new file mode 100644 index 00000000..fa01322c --- /dev/null +++ b/app/(loggedin)/layout.tsx @@ -0,0 +1,23 @@ +'use client' + +import { Authenticator } from '@aws-amplify/ui-react'; +import '@aws-amplify/ui-react/styles.css'; + +export default function LoggedInLayout({ children }: { children: React.ReactNode }) { + return ( + + {({ user, signOut }) => ( + <> + + {children} + + )} + + ); +} \ No newline at end of file diff --git a/app/(loggedin)/sign-in/page.tsx b/app/(loggedin)/sign-in/page.tsx new file mode 100644 index 00000000..1a0f72b6 --- /dev/null +++ b/app/(loggedin)/sign-in/page.tsx @@ -0,0 +1,7 @@ +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +export default function SignInPage() { + revalidatePath('/'); + redirect('/'); +} \ No newline at end of file diff --git a/app/_actions/actions.ts b/app/_actions/actions.ts new file mode 100644 index 00000000..fafed3cf --- /dev/null +++ b/app/_actions/actions.ts @@ -0,0 +1,121 @@ +'use server' + +import { cookieBasedClient } from '@/utils/amplifyServerUtils'; +import { authSession } from '@/utils/amplifyServerUtils'; +import { z } from 'zod'; +import { revalidateTag } from 'next/cache'; +import { redirect } from 'next/navigation'; +import { SNSClient, SubscribeCommand } from '@aws-sdk/client-sns'; + +import amplify from '@/amplify_outputs.json'; + +const linkSuggestionSchema = z.object({ + url: z.string().url(), + comment: z.string().optional() +}); + +export const fetchPosts = async (limit: number = 3) => { + const { data: posts, errors } = await cookieBasedClient.models.Post.list({ + limit, + selectionSet: [ + 'id', + 'title', + 'banner', + 'url', + 'content', + 'categories', + 'createdAt', + 'updatedAt', + 'author.*', + ], + sortDirection: 'DESC' + }); + if (errors) { + console.error(errors); + } + return posts; +}; + +// pending fix https://github.com/aws-amplify/amplify-category-api/issues/2621 +export const fetchMostRecentPosts = async (limit: number = 3) => { + const posts = await fetchPosts(200); + return posts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit); +}; + +export const addLinkSuggestion = async (prevState: any, formData: FormData) => { + const parsedData = linkSuggestionSchema.safeParse({ + url: formData.get('url'), + comment: formData.get('comment'), + }); + + if (!parsedData.success) { + console.error(parsedData.error); + return { + errors: parsedData.error.message + }; + } + + const { data: linkSuggestion, errors } = await cookieBasedClient.models.LinkSuggestion.create(parsedData.data); + console.log({linkSuggestion}); + if (errors) { + console.error(errors); + } + revalidateTag('posts') // Update cached posts + redirect('/') +}; + +export const fetchResources = async (limit: number = 3) => { + const { data: resources, errors } = await cookieBasedClient.models.Resource.list({ + limit, + selectionSet: [ + 'id', + 'title', + 'content', + 'url', + 'categories', + 'banner', + 'createdAt', + 'updatedAt', + ], + sortDirection: 'DESC' + }); + if (errors) { + console.error(errors); + } + return resources; +}; + +// pending fix https://github.com/aws-amplify/amplify-category-api/issues/2621 +export const fetchMostRecentResources = async (limit: number = 3) => { + const resources = await fetchResources(200); + return resources.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit); +}; + +export const subscribeToNewsletter = async (formData: FormData) => { + const session = await authSession(); + console.log({session}); + + if (!session) { + throw new Error('User is not authenticated'); + } + + const snsClient = new SNSClient({ + credentials: session.credentials, + region: amplify.auth.aws_region, + }); + + const email = formData.get('email') as string; + + try { + const command = new SubscribeCommand({ + Protocol: 'email', + TopicArn: amplify.custom.subscribersTopicArn, + Endpoint: email, + }); + + const response = await snsClient.send(command); + console.log('Subscription successful:', response); + } catch (error) { + console.error('Error subscribing to SNS:', error); + } +}; \ No newline at end of file diff --git a/frontend/src/components/.gitkeep b/app/_components/.gitkeep similarity index 100% rename from frontend/src/components/.gitkeep rename to app/_components/.gitkeep diff --git a/app/_components/Avatar.tsx b/app/_components/Avatar.tsx new file mode 100644 index 00000000..7ca2e723 --- /dev/null +++ b/app/_components/Avatar.tsx @@ -0,0 +1,31 @@ +import { getUrl } from 'aws-amplify/storage/server'; +import Image from 'next/image'; +import { runWithAmplifyServerContext } from '@/utils/amplifyServerUtils'; + +// Re-render this page every 60 minutes +export const revalidate = 60 * 60; // in seconds + +export default async function Avatar({ avatarKey }: { avatarKey: string }) { + try { + const avatarUrl = await runWithAmplifyServerContext({ + nextServerContext: null, + operation: (contextSpec) => + getUrl(contextSpec, { + path: avatarKey + }) + }); + + return ( + Avatar Image + ); + } catch (error) { + console.error(error); + return

Something went wrong...

; + } +} \ No newline at end of file diff --git a/app/_components/CreateContent.tsx b/app/_components/CreateContent.tsx new file mode 100644 index 00000000..c58ff6a1 --- /dev/null +++ b/app/_components/CreateContent.tsx @@ -0,0 +1,27 @@ +import React, { ReactElement } from "react" +import Link from 'next/link' + +function CreateContent(): ReactElement { + return ( + <> +
+
+

+ Created or seen other CDK related content? +
+ Share it with the community +

+
+
+ + Suggest a Link + +
+
+
+
+ + ) +} + +export default CreateContent diff --git a/app/_components/Footer.tsx b/app/_components/Footer.tsx new file mode 100644 index 00000000..ab37cee0 --- /dev/null +++ b/app/_components/Footer.tsx @@ -0,0 +1,109 @@ +import React, { ReactElement } from "react" +import Link from "next/link" + +function Footer(): ReactElement { + return ( +
+
+ +
+ + Slack + + + + + + Twitter + + + + + + GitHub + + + + +
+
+

+ © 2020-2023 - Made with{" "} + + 💚 + {" "} + by cdk.dev Community / Open Construct Foundation +

+
+
+
+ ) +} + +export default Footer diff --git a/app/_components/Hero.tsx b/app/_components/Hero.tsx new file mode 100644 index 00000000..d6d182d7 --- /dev/null +++ b/app/_components/Hero.tsx @@ -0,0 +1,285 @@ +'use client' + +import React, { ReactElement } from "react" +import Link from "next/link" +import { useState } from "react" +import { Transition } from "@tailwindui/react" + +export interface NavProps { + title?: string +} + +function HeroPattern(): ReactElement { + return ( +
+
+ + + + + + + + + + + + + + + + +
+
+ ) +} + +function Nav({ title }: NavProps): ReactElement { + const pageTitle = title || "" + const [isOpen, setIsOpen] = useState(false) + + return ( + <> +
+ +
+
+ +
+ + + {(ref) => ( +
+
+
+
+
+ cdk.dev +
+
+ +
+
+ +
+
+
+ )} +
+
+
+

+ Welcome to cdk + .dev +

+

+ The community driven hub around the Cloud Development Kit (CDK) + ecosystem. This site brings together all the latest blogs, + videos, and educational content. Connect with the community of + AWS CDK, CDK for Kubernetes (cdk8s) and CDK for Terraform + (cdktf). +

+ + +
+
+
+
+ + ) +} + +export default Nav diff --git a/app/_components/HostnameLink.tsx b/app/_components/HostnameLink.tsx new file mode 100644 index 00000000..fc2a6d8f --- /dev/null +++ b/app/_components/HostnameLink.tsx @@ -0,0 +1,17 @@ +import { ReactElement } from "react"; +import Link from "next/link"; + +interface HostnameLinkProps { + url: string; +} + +function HostnameLink({ url }: HostnameLinkProps): ReactElement { + const hostname = new URL(url).hostname; + return ( + + {hostname} + + ); +} + +export default HostnameLink; \ No newline at end of file diff --git a/app/_components/Logos.tsx b/app/_components/Logos.tsx new file mode 100644 index 00000000..b006a0ec --- /dev/null +++ b/app/_components/Logos.tsx @@ -0,0 +1,55 @@ +import React, { ReactElement } from "react" +import Image from "next/image" +import terraform from "@/app/_components/logos/terraform.svg" +import aws from "@/app/_components/logos/aws.svg" +import kubernetes from "@/app/_components/logos/kubernetes.svg" + +function Logos(): ReactElement { + return ( + <> +
+
+
+
+
+
+
+
+ + Terraform + +
+
+ + AWS + +
+ +
+ + Kubernetes + +
+
+
+
+
+
+
+ + ) +} + +export default Logos diff --git a/app/_components/Nav.tsx b/app/_components/Nav.tsx new file mode 100644 index 00000000..2eda70b8 --- /dev/null +++ b/app/_components/Nav.tsx @@ -0,0 +1,209 @@ +'use client' + +import React, { ReactElement } from "react" +import NavLink from "./NavLink" +import { useState } from "react" +import { Transition } from "@tailwindui/react" +import Image from 'next/image' +import logo from '@/public/cdkdevlogo.svg' + +export interface NavProps { + title?: string +} + +function Nav({ title }: NavProps): ReactElement { + const pageTitle = title || "" + const [isOpen, setIsOpen] = useState(false) + + return ( + <> + + + + {(ref) => ( +
+
+
+
+
+ cdk.dev +
+
+ +
+
+ +
+
+
+ )} +
+ + ) +} + +export default Nav diff --git a/app/_components/NavLink.tsx b/app/_components/NavLink.tsx new file mode 100644 index 00000000..28f108df --- /dev/null +++ b/app/_components/NavLink.tsx @@ -0,0 +1,26 @@ +import { usePathname } from "next/navigation" +import Link from "next/link" + +type Props = { + href: string + linkName: string +} + +const NavLink = ({ href, linkName }: Props) => { + const activeClassName = "border-indigo-500", + inactiveClassName = "border-transparent" + + const pathname = usePathname() + + const linkClasses = `inline-flex items-center px-1 pt-1 border-b-2 ${ + pathname === href ? activeClassName : inactiveClassName + } text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out` + + return ( + + {linkName} + + ) +} + +export default NavLink \ No newline at end of file diff --git a/app/_components/Newsletter.tsx b/app/_components/Newsletter.tsx new file mode 100644 index 00000000..4a967f26 --- /dev/null +++ b/app/_components/Newsletter.tsx @@ -0,0 +1,37 @@ + +import React, { ReactElement } from "react" +import { subscribeToNewsletter } from "../_actions/actions" + +function Newsletter(): ReactElement { + return ( +
+
+
+

+ Get notified about new posts +

+

+ No spam, no noise. Just the latest posts. +

+
+
+
+ + +
+ +
+
+
+
By subscribing, you agree to receive the latest posts from the CDK team.
+
+
+
+
+ + ) +} + +export default Newsletter \ No newline at end of file diff --git a/app/_components/Post.tsx b/app/_components/Post.tsx new file mode 100644 index 00000000..95661b00 --- /dev/null +++ b/app/_components/Post.tsx @@ -0,0 +1,91 @@ +import { ReactElement } from "react" +import dayjs from "dayjs" +import utc from "dayjs/plugin/utc" +import timezone from "dayjs/plugin/timezone" +import relativeTime from "dayjs/plugin/relativeTime" +import Avatar from "./Avatar" +import PostImage from "./PostImage" +import HostnameLink from "./HostnameLink" +import Link from "next/link" + +dayjs.extend(relativeTime) // For fromNow() +dayjs.extend(utc) // From Timezone +dayjs.extend(timezone) + +interface PostProps { + post: { + url: string; + title: string; + banner: string | null; + content: string; + categories: string[] | null; + author: { + avatar: string; + name: string; + }; + createdAt: string; + }; +} + +function Post({ post }: PostProps): ReactElement { + return ( +
+
+ + + +
+
+
+ +

+ {post.title} +

+

+ {post.content} +

+ +
+
+ {post.categories?.map((category, index) => ( + + {category} + + ))} +
+
+
+ + + +
+
+

+ + {post.author.name} + +

+
+ added  + + · + +
+
+
+
+
+ ) +} + +export default Post diff --git a/app/_components/PostImage.tsx b/app/_components/PostImage.tsx new file mode 100644 index 00000000..40b2c648 --- /dev/null +++ b/app/_components/PostImage.tsx @@ -0,0 +1,46 @@ +import { getUrl } from 'aws-amplify/storage/server'; +import Image from 'next/image'; +import { runWithAmplifyServerContext } from '@/utils/amplifyServerUtils'; +import GeoPattern from "geopattern" + +// Re-render this page every 60 minutes +export const revalidate = 60 * 60; // in seconds + +export default async function PostImage({ postTitle, postKey }: { postTitle: string, postKey: string | null }) { + try { + if (!postKey) { + const pattern = GeoPattern.generate(postTitle) + const postImageUrl = pattern.toDataUri() + return Post Image + } + + const postImageUrl = await runWithAmplifyServerContext({ + nextServerContext: null, + operation: (contextSpec) => + getUrl(contextSpec, { + path: postKey + }) + }); + + return ( + {postTitle} + ); + } catch (error) { + console.error(error); + return

Something went wrong...

; + } +} \ No newline at end of file diff --git a/app/_components/logos/aws.svg b/app/_components/logos/aws.svg new file mode 100644 index 00000000..80192a12 --- /dev/null +++ b/app/_components/logos/aws.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/app/_components/logos/kubernetes.svg b/app/_components/logos/kubernetes.svg new file mode 100644 index 00000000..3940b20f --- /dev/null +++ b/app/_components/logos/kubernetes.svg @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/_components/logos/terraform.svg b/app/_components/logos/terraform.svg new file mode 100644 index 00000000..61fc804d --- /dev/null +++ b/app/_components/logos/terraform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/amplify.ts b/app/amplify.ts new file mode 100644 index 00000000..e5870b1c --- /dev/null +++ b/app/amplify.ts @@ -0,0 +1,10 @@ +'use client'; + +import { Amplify } from 'aws-amplify'; +import outputs from '../amplify_outputs.json'; + +Amplify.configure(outputs, { ssr: true }); + +export default function ConfigureAmplifyClientSide() { + return null; +} \ No newline at end of file diff --git a/app/codeofconduct/page.tsx b/app/codeofconduct/page.tsx new file mode 100644 index 00000000..1e07aa9a --- /dev/null +++ b/app/codeofconduct/page.tsx @@ -0,0 +1,175 @@ +import Nav from "@/app/_components/Nav"; +import CreateContent from "@/app/_components/CreateContent"; +import Newsletter from "@/app/_components/Newsletter"; +import React from "react"; + +const coc = () => { + return ( + <> +