├── .eslintrc.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── components ├── ConversationBar │ ├── ConversationBar.js │ └── index.js ├── InputArea │ ├── InputArea.js │ └── index.js ├── Message │ ├── MessageItem.js │ ├── MessageList.js │ └── index.js └── RoomList │ ├── RoomList.js │ └── index.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js ├── index.js └── rooms │ └── [roomId].js ├── public ├── favicon.ico └── vercel.svg ├── src └── sample.aws-exports.js └── styles ├── Home.module.css └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | #amplify-do-not-edit-begin 37 | amplify/\#current-cloud-backend 38 | amplify/.config/local-* 39 | amplify/logs 40 | amplify/mock-data 41 | amplify/backend/amplify-meta.json 42 | amplify/backend/.temp 43 | build/ 44 | dist/ 45 | node_modules/ 46 | aws-exports.js 47 | awsconfiguration.json 48 | amplifyconfiguration.json 49 | amplifyconfiguration.dart 50 | amplify-build-config.json 51 | amplify-gradle-config.json 52 | amplifytools.xcconfig 53 | .secret-* 54 | **.sample 55 | #amplify-do-not-edit-end 56 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Modern Chat app Frontend 2 | 3 | 🚨 Note that this project does not create our backend services via the Amplify CLI, but relies on exported values from the AWS CDK. 4 | 5 | 🚨 This application is part of a blog post that explains both the frontend and the backend as a whole. The backend repo can be found [here](https://github.com/Focus-Otter/fullstack-cdk-helpers/blob/main/README.md) 6 | 7 | ![ezgif-5-31ccf9d033](https://user-images.githubusercontent.com/5106417/184162547-1a3ab9b4-8f91-4a81-be58-f6af35469e02.gif) 8 | 9 | ## Overview 10 | 11 | This repo is the frontend to building a fullstack chat app. The backend can be found [here](https://github.com/Focus-Otter/chat-cdk-backend). 12 | 13 | ![image](https://user-images.githubusercontent.com/5106417/184164922-9cbe806e-bb41-4341-89f5-eb419df915b3.png) 14 | 15 | ## Tech Stack 16 | 17 | - React Framework: NextJS 18 | - UI Library: AWS Amplify UI primitives 19 | - API: GraphQL via AWS AppSync 20 | - File uploads: Sent to Amazon S3 21 | - Signup/SignIn: Managed with Cognito 22 | - Backend Binding: amplify-js 23 | 24 | ## Steps to get started 25 | 26 | Once [the backend ](https://github.com/Focus-Otter/fullstack-cdk-helpers/blob/main/README.md) is deployed, it will output a set of values. The outputted values are what you'll need to get this project working. 27 | 28 | 1. Run `amplify init` 29 | 2. Run `amplify add codegen --apiId YOUR_APPID` (value generated from the backend) 30 | 3. `amplify codegen` (accept the defaults, but set the max-depth to 4) 31 | 4. Create a `src/aws-exports.js` file and bring over the values from your CDK backend and ensure your project looks like the `sample.aws-exports.js` file. 32 | 5. Run the app and create 2 users 33 | 6. Once signed in, create a room. 34 | 7. You should now be able to view the rooms on the homepage. Click one and begin creating messages. 35 | 36 | ## Security 37 | 38 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 39 | 40 | ## License 41 | 42 | This library is licensed under the MIT-0 License. See the LICENSE file. 43 | -------------------------------------------------------------------------------- /components/ConversationBar/ConversationBar.js: -------------------------------------------------------------------------------- 1 | import { Flex, Menu, useBreakpointValue } from '@aws-amplify/ui-react' 2 | import { useRouter } from 'next/router' 3 | import { useState } from 'react' 4 | import { RoomList } from '../RoomList' 5 | 6 | export const ConversationBar = ({ rooms = [], onRoomChange }) => { 7 | const [isMenuOpen, setIsMenuOpen] = useState(false) 8 | const variation = useBreakpointValue({ 9 | base: 'isMobile', 10 | medium: 'isTabletOrHigher', 11 | }) 12 | const toggleMenu = (roomId) => { 13 | setIsMenuOpen(false) 14 | onRoomChange(roomId) 15 | } 16 | 17 | const ConversationDisplay = ({ rooms = [] }) => { 18 | if (variation === 'isMobile') { 19 | return ( 20 | 21 | { 25 | setIsMenuOpen(!isMenuOpen) 26 | }} 27 | > 28 | 29 | 30 | 31 | ) 32 | } else if (variation === 'isTabletOrHigher') { 33 | return 34 | } 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /components/ConversationBar/index.js: -------------------------------------------------------------------------------- 1 | export { ConversationBar } from './ConversationBar' 2 | -------------------------------------------------------------------------------- /components/InputArea/InputArea.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Flex, 4 | TextAreaField, 5 | TextField, 6 | View, 7 | } from '@aws-amplify/ui-react' 8 | import { Storage } from 'aws-amplify' 9 | import { useState } from 'react' 10 | 11 | export const InputArea = ({ onMessageSend }) => { 12 | const [selectedImage, setSelectedImage] = useState(null) 13 | const [messageText, setMessageText] = useState('') 14 | 15 | const uploadFile = async (selectedPic) => { 16 | const { key } = await Storage.put(selectedPic.name, selectedPic, { 17 | contentType: selectedPic.type, 18 | }) 19 | 20 | return key 21 | } 22 | 23 | const handleFormSubmit = async (e) => { 24 | e.preventDefault() 25 | let key 26 | if (selectedImage) { 27 | key = await uploadFile(selectedImage) 28 | } 29 | 30 | onMessageSend(messageText, key) 31 | setMessageText('') 32 | } 33 | return ( 34 | 40 | 41 |
42 | { 46 | setMessageText(e.target.value) 47 | }} 48 | value={messageText} 49 | /> 50 |
51 | 52 | setSelectedImage(e.target.files[0])} 55 | /> 56 | 59 | 60 | 61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /components/InputArea/index.js: -------------------------------------------------------------------------------- 1 | export { InputArea } from './InputArea' 2 | -------------------------------------------------------------------------------- /components/Message/MessageItem.js: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Flex, 4 | Heading, 5 | Image, 6 | Text, 7 | useTheme, 8 | View, 9 | } from '@aws-amplify/ui-react' 10 | import { Storage } from 'aws-amplify' 11 | import { useEffect, useState } from 'react' 12 | 13 | export const MessageItem = ({ msg = {}, myUsername }) => { 14 | const { tokens } = useTheme() 15 | if (msg.content.imageId) { 16 | // console.log('the message', msg) 17 | } 18 | const isMyMsg = msg.owner === myUsername 19 | const isEdited = msg.createdAt !== msg.updatedAt 20 | 21 | return ( 22 | 29 | 30 | avatar 37 | 38 | 39 | 40 | 41 | {msg.owner}{' '} 42 | 48 | {msg.createdAt} 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | const TextMessage = ({ isMyMsg, msgContent, isEdited }) => { 66 | return ( 67 | 68 | {msgContent}{' '} 69 | 70 | ) 71 | } 72 | 73 | const PicMessage = ({ msgContent }) => { 74 | const [picUrl, setPicUrl] = useState('') 75 | console.log(msgContent) 76 | useEffect(() => { 77 | Storage.get(msgContent).then((url) => { 78 | console.log(url) 79 | setPicUrl(url) 80 | }) 81 | }, [msgContent]) 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /components/Message/MessageList.js: -------------------------------------------------------------------------------- 1 | import { Flex } from '@aws-amplify/ui-react' 2 | import { MessageItem } from './index' 3 | 4 | export const MessageList = ({ messages = [], myUsername }) => { 5 | return ( 6 | 13 | {messages.map((msg) => ( 14 | 15 | ))} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/Message/index.js: -------------------------------------------------------------------------------- 1 | export { MessageItem } from './MessageItem' 2 | export { MessageList } from './MessageList' 3 | -------------------------------------------------------------------------------- /components/RoomList/RoomList.js: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableRow, 7 | View, 8 | } from '@aws-amplify/ui-react' 9 | 10 | export const RoomList = ({ handleMenuToggle, rooms = [] }) => { 11 | return ( 12 | 13 | 14 | 15 | 16 | Application Rooms 17 | 18 | 19 | 20 | {rooms.map((room) => ( 21 | { 24 | console.log(room.id) 25 | handleMenuToggle(room.id) 26 | }} 27 | > 28 | {room.name} 29 | 30 | ))} 31 | 32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/RoomList/index.js: -------------------------------------------------------------------------------- 1 | export { RoomList } from './RoomList' 2 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsync-chat-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@aws-amplify/ui-react": "^3.0.4", 13 | "aws-amplify": "^4.3.27", 14 | "next": "13.5.4", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "react-infinite-scroller": "^1.2.6" 18 | }, 19 | "devDependencies": { 20 | "eslint": "8.19.0", 21 | "eslint-config-next": "12.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { Amplify } from 'aws-amplify' 2 | import { AmplifyProvider } from '@aws-amplify/ui-react' 3 | import config from '../src/aws-exports' 4 | import '@aws-amplify/ui-react/styles.css' 5 | 6 | Amplify.configure({ ...config, ssr: true }) 7 | function MyApp({ Component, pageProps }) { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | export default MyApp 16 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Flex, 4 | Link, 5 | Text, 6 | TextField, 7 | View, 8 | withAuthenticator, 9 | } from '@aws-amplify/ui-react' 10 | import { API } from 'aws-amplify' 11 | import { useEffect, useState } from 'react' 12 | import { listRooms } from '../src/graphql/queries' 13 | import { createRoom } from '../src/graphql/mutations' 14 | import NextLink from 'next/link' 15 | import config from '../src/aws-exports' 16 | import { Amplify } from 'aws-amplify' 17 | 18 | Amplify.configure({ ...config, ssr: true }) 19 | 20 | function Home({ signOut, user }) { 21 | const [rooms, setRooms] = useState([]) 22 | const [roomName, setRoomName] = useState('') 23 | 24 | useEffect(() => { 25 | API.graphql({ 26 | query: listRooms, 27 | }).then(({ data }) => { 28 | setRooms(data.listRooms.items) 29 | }) 30 | }, []) 31 | 32 | const handleSubmit = async (e) => { 33 | e.preventDefault() 34 | const { data } = await API.graphql({ 35 | query: createRoom, 36 | variables: { 37 | input: { 38 | name: roomName, 39 | }, 40 | }, 41 | }) 42 | 43 | setRooms([...rooms, data.createRoom]) 44 | } 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | 52 | Hey, {user.username}! Select a room to chat in or create your own public 53 | room. 54 | 55 |
56 | setRoomName(e.target.value)} 61 | /> 62 | 63 |
    64 | {rooms.map((room) => ( 65 |
  • 66 | 67 | {room.name} 68 | 69 |
  • 70 | ))} 71 |
72 |
73 | ) 74 | } 75 | 76 | export default withAuthenticator(Home, { 77 | signUpAttributes: ['email', 'given_name', 'family_name'], 78 | }) 79 | -------------------------------------------------------------------------------- /pages/rooms/[roomId].js: -------------------------------------------------------------------------------- 1 | import { Flex, Heading, useTheme, View } from '@aws-amplify/ui-react' 2 | import { useEffect, useState } from 'react' 3 | import { withSSRContext } from 'aws-amplify' 4 | import { InputArea } from '../../components/InputArea' 5 | import { MessageList } from '../../components/Message' 6 | import { ConversationBar } from '../../components/ConversationBar' 7 | import config from '../../src/aws-exports' 8 | import { Amplify, API } from 'aws-amplify' 9 | import { listMessagesForRoom, listRooms } from '../../src/graphql/queries' 10 | import { createMessage } from '../../src/graphql/mutations' 11 | import { onCreateMessageByRoomId } from '../../src/graphql/subscriptions' 12 | import { useRouter } from 'next/router' 13 | Amplify.configure({ ...config, ssr: true }) 14 | 15 | function RoomPage({ roomsList, currentRoomData, username }) { 16 | console.log(username) 17 | const { tokens } = useTheme() 18 | const router = useRouter() 19 | const [messages, setMessages] = useState([]) 20 | const [rooms, setRooms] = useState(roomsList) 21 | const [currentRoom, setCurrentRoom] = useState(currentRoomData) 22 | 23 | const handleMessageSend = async (newMessage, imgKey) => { 24 | const createNewMsg = async (text, imageId) => { 25 | let content = { text, imageId } 26 | return await API.graphql({ 27 | query: createMessage, 28 | variables: { 29 | input: { 30 | content, 31 | roomId: currentRoom.id, 32 | }, 33 | }, 34 | }) 35 | } 36 | if (newMessage && !imgKey) { 37 | createNewMsg(newMessage).then(({ data }) => 38 | setMessages([data.createMessage, ...messages]) 39 | ) 40 | } else if (!newMessage && imgKey) { 41 | console.log('the imgkey', imgKey) 42 | createNewMsg(undefined, imgKey).then(({ data }) => 43 | setMessages([data.createMessage, ...messages]) 44 | ) 45 | } else if (newMessage && imgKey) { 46 | createNewMsg(newMessage, imgKey).then(({ data }) => 47 | setMessages([data.createMessage, ...messages]) 48 | ) 49 | } 50 | } 51 | 52 | const handleRoomChange = (roomID) => { 53 | const newRoom = rooms.find((room) => room.id === roomID) 54 | setCurrentRoom(newRoom) 55 | router.push(`/rooms/${roomID}`) 56 | } 57 | 58 | useEffect(() => { 59 | API.graphql({ 60 | query: listMessagesForRoom, 61 | variables: { 62 | roomId: currentRoom.id, 63 | sortDirection: 'DESC', 64 | }, 65 | }).then(({ data }) => setMessages(data.listMessagesForRoom.items)) 66 | }, [currentRoom.id]) 67 | 68 | useEffect(() => { 69 | const subscription = API.graphql({ 70 | query: onCreateMessageByRoomId, 71 | variables: { roomId: currentRoom.id }, 72 | }).subscribe({ 73 | next: ({ value }) => { 74 | if (value.data.onCreateMessageByRoomId.owner !== username) { 75 | console.log(value.data.onCreateMessageByRoomId) 76 | setMessages((currMsgs) => [ 77 | value.data.onCreateMessageByRoomId, 78 | ...currMsgs, 79 | ]) 80 | } 81 | }, 82 | }) 83 | 84 | return () => subscription.unsubscribe() 85 | }, [currentRoom.id, username]) 86 | 87 | return ( 88 | <> 89 | 90 | 91 | 92 | 93 | 94 | 101 | {currentRoom.name} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ) 113 | } 114 | 115 | export default RoomPage 116 | 117 | export async function getServerSideProps(context) { 118 | const { API, Auth } = withSSRContext(context) 119 | try { 120 | const user = await Auth.currentAuthenticatedUser() 121 | const { data } = await API.graphql({ 122 | query: listRooms, 123 | }) 124 | 125 | const currentRoomData = data.listRooms.items.find( 126 | (room) => room.id === context.params.roomId 127 | ) 128 | 129 | return { 130 | props: { 131 | currentRoomData, 132 | username: user.username, 133 | roomsList: data.listRooms.items, 134 | }, 135 | } 136 | } catch (err) { 137 | return { 138 | redirect: { 139 | destination: '/', 140 | permanent: false, 141 | }, 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amplify-nextjs-chat-app/8b5a5f663f3c8418b2af6352a61344a181a03b6e/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/sample.aws-exports.js: -------------------------------------------------------------------------------- 1 | // note that Auth is top-level, whereas the API stuff is not 2 | const awsmobile = { 3 | Auth: { 4 | region: 'your-region', 5 | userPoolId: 'your-region_YOUR_REGION_ID', 6 | userPoolWebClientId: '6vstsuciYOURCLIENTID', 7 | identityPoolId: 'identitypoolID', 8 | 9 | }, 10 | Storage: { 11 | AWSS3: { 12 | bucket: 'bucket-name', 13 | region: 'your-region', 14 | }, 15 | }, 16 | aws_project_region: 'your-region', 17 | aws_appsync_graphqlEndpoint: 18 | 'https://your-api.appsync-api.your-region.amazonaws.com/graphql', 19 | aws_appsync_region: 'your-region', 20 | aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS', 21 | } 22 | 23 | export default awsmobile 24 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | } 10 | 11 | .main { 12 | padding: 5rem 0; 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | align-items: center; 18 | } 19 | 20 | .footer { 21 | width: 100%; 22 | height: 100px; 23 | border-top: 1px solid #eaeaea; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer a { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-grow: 1; 34 | } 35 | 36 | .title a { 37 | color: #0070f3; 38 | text-decoration: none; 39 | } 40 | 41 | .title a:hover, 42 | .title a:focus, 43 | .title a:active { 44 | text-decoration: underline; 45 | } 46 | 47 | .title { 48 | margin: 0; 49 | line-height: 1.15; 50 | font-size: 4rem; 51 | } 52 | 53 | .title, 54 | .description { 55 | text-align: center; 56 | } 57 | 58 | .description { 59 | line-height: 1.5; 60 | font-size: 1.5rem; 61 | } 62 | 63 | .code { 64 | background: #fafafa; 65 | border-radius: 5px; 66 | padding: 0.75rem; 67 | font-size: 1.1rem; 68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 69 | Bitstream Vera Sans Mono, Courier New, monospace; 70 | } 71 | 72 | .grid { 73 | display: flex; 74 | align-items: center; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | max-width: 800px; 78 | margin-top: 3rem; 79 | } 80 | 81 | .card { 82 | margin: 1rem; 83 | padding: 1.5rem; 84 | text-align: left; 85 | color: inherit; 86 | text-decoration: none; 87 | border: 1px solid #eaeaea; 88 | border-radius: 10px; 89 | transition: color 0.15s ease, border-color 0.15s ease; 90 | width: 45%; 91 | } 92 | 93 | .card:hover, 94 | .card:focus, 95 | .card:active { 96 | color: #0070f3; 97 | border-color: #0070f3; 98 | } 99 | 100 | .card h2 { 101 | margin: 0 0 1rem 0; 102 | font-size: 1.5rem; 103 | } 104 | 105 | .card p { 106 | margin: 0; 107 | font-size: 1.25rem; 108 | line-height: 1.5; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | @media (max-width: 600px) { 117 | .grid { 118 | width: 100%; 119 | flex-direction: column; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | #__next, 3 | body { 4 | padding: 0; 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 7 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 8 | } 9 | 10 | a { 11 | color: inherit; 12 | text-decoration: none; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | /* styles.css */ 20 | 21 | .amplify-textareafield { 22 | --amplify-components-fieldcontrol-color: black; 23 | --amplify-components-fieldcontrol-border-color: rgba(255, 0, 0, 0); 24 | --amplify-components-fieldcontrol-focus-border-color: rgba(0, 0, 255, 0); 25 | } 26 | --------------------------------------------------------------------------------