Sample app frontend (#5931)

* Add darkmode

* Edit README

* Add comments and readme

* Fix UI

* Update README

* Update code-samples/eventing/bookstore-sample-app/frontend/client/pages/Main.js

Co-authored-by: Leo Li <leoli@redhat.com>

* Update code-samples/eventing/bookstore-sample-app/frontend/client/components/BookDetail.js

Co-authored-by: Leo Li <leoli@redhat.com>

* Change emoji

* Update code-samples/eventing/bookstore-sample-app/frontend/client/components/Toggle.js

Co-authored-by: Leo Li <leoli@redhat.com>

* Fix UI

* Align time

---------

Co-authored-by: Leo Li <leoli@redhat.com>
This commit is contained in:
Phuong Cao 2024-04-05 11:09:02 -04:00 committed by GitHub
parent bf326bb372
commit 791daf51bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 8094 additions and 0 deletions

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# 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
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -0,0 +1,23 @@
# Use a base image with Node.js LTS
FROM node:lts-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of your application code to the working directory
COPY . .
# Build the Next.js application
RUN npm run build
# Expose the port your app runs on
EXPOSE 3000
# Define the command to run your app
CMD ["npm", "run dev"]

View File

@ -0,0 +1,57 @@
# Getting Started
This app use Next.js and TailwindCSS as main packages. Use this command to install all dependencies:
```bash
npm install
```
To run application, use:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
# Project Structures
- app/: Contains the main layout, page, and global styling.
- client/: Contains components and pages used in the application.
- public/images/: Contains image files.
- next-env.d.ts, next.config.mjs, package-lock.json, package.json, postcss.config.js, tailwind.config.js, tsconfig.json: Configuration files for Next.js, Tailwind CSS, and TypeScript.
# Containerize Application
This repository contains a Next.js application that utilizes next-themes and Tailwind CSS. This README file provides instructions on how to containerize the application using Docker.
## Prerequisites
- Docker installed on your machine. You can download and install Docker from [here](https://www.docker.com/get-started).
## Dockerization Steps
1. Clone this repository to your local machine.
2. Navigate to the root directory of the cloned repository.
### Building the Docker Image
Run the following command to build the Docker image:
```bash
docker build -t frontend .
```
## Running the Docker Container
Once the image is built, you can run a container using the following command:
```bash
docker run -d -p 3000:3000 frontend
```

View File

@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
#__next {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
}
body {
font-family: 'Poppins', sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

View File

@ -0,0 +1,25 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Knative Bookstore',
description: 'Bookstore Sample Application from Knative',
icons: {
icon: '/images/knative-logo.png',
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@ -0,0 +1,11 @@
'use client';
import Main from '../client/pages/Main';
import { ThemeProvider } from 'next-themes';
export default function Home() {
return (
<ThemeProvider attribute='class'>
<Main />
</ThemeProvider>
);
}

View File

@ -0,0 +1,43 @@
const BookDetail = ({ book }) => {
return (
<div className='flex flex-col md:flex-row md:items-start p-8 space-x-8 justify-center items-center font-sans'>
<div className='mb-4 md:mb-0 md:mr-8'>
<img src={book.img} alt='Book Cover' className='w-48 h-auto' />
</div>
<div>
<div className='space-y-4'>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Title:</span>
<span className='text-left'>{book.title}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Author:</span>
<span className='text-left'>{book.author}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>ISBN:</span>
<span>{book.ISBN}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Publisher:</span>
<span>{book.publisher}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Published Date:</span>
<span>{book.publishedDate}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Description:</span>
<span>{book.description}</span>
</div>
<div className='grid grid-cols-[auto_1fr] gap-x-2 items-center'>
<span className='font-bold text-left'>Price:</span>
<span>{book.price}</span>
</div>
</div>
</div>
</div>
);
};
export default BookDetail;

View File

@ -0,0 +1,38 @@
import Emoji from './Emoji';
const CommentDisplay = ({ comment }) => {
// Assume receiving a comment object
let emoji;
if (comment.emotion === 'Positive') {
emoji = '😃';
} else if (comment.emotion === 'Neutral') {
emoji = '😐';
} else {
emoji = '😡';
}
return (
<div className='flex my-4 p-4 justify-center align-middle items-center'>
<div className='comment-display w-full w-7/12 flex flex-row rounded-lg p-4 bg-gray-800 text-white dark:bg-white dark:text-black'>
<div className='flex items-center justify-center md:w-1/12'>
<img
src={comment.avatar}
alt='Avatar'
className='rounded-full w-8 h-8'
/>
</div>
<div className='md:w-1/12 flex items-center content-center text-gray-200 dark:text-black'>
{comment.time}
</div>
<div className='md:w-9/12 '>
<span className='h-full flex items-center content-center'>
{comment.text}
</span>
</div>
<div className='md:w-1/12 text-4xl flex items-center content-center'>
<Emoji symbol={emoji} label={comment.emotion} size='text-2xl' />
</div>
</div>
</div>
);
};
export default CommentDisplay;

View File

@ -0,0 +1,44 @@
'use-client';
import { useState } from 'react';
const CommentForm = () => {
const [hover, setHover] = useState(false);
const [comment, setComment] = useState('');
const handleInputChange = (event) => {
setComment(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Submitted comment:', comment); // Use inspect to see
};
return (
<div className='flex my-4 p-4 justify-center'>
<form
className='w-full w-8/12 flex flex-col items-end '
onSubmit={handleSubmit}
>
<textarea
className='form-textarea w-full mb-2 p-2 border border-2 border-black rounded-lg p-4'
rows='3'
placeholder='Leave your comment here...'
value={comment}
onChange={handleInputChange}
></textarea>
<button
type='submit'
className={`font-bold py-2 px-9 rounded ${
hover ? '' : 'bg-blue-600'
}`}
style={{ backgroundColor: hover ? '#A0DDFF' : '#A5D8FF' }}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
Submit
</button>
</form>
</div>
);
};
export default CommentForm;

View File

@ -0,0 +1,12 @@
import CommentDisplay from './CommentDisplay';
const CommentList = () => {
const comment = {
avatar: '/images/avatar.jpg',
time: '10:05',
text: 'I used this provider to insert a different theme object depending on a person ',
emotion: 'Neutral',
};
return <CommentDisplay comment={comment} />;
};
export default CommentList;

View File

@ -0,0 +1,12 @@
import React from 'react';
const Emoji = (props) => (
<span
className='emoji'
role='img'
aria-label={props.label ? props.label : ''}
aria-hidden={props.label ? 'false' : 'true'}
>
{props.symbol}
</span>
);
export default Emoji;

View File

@ -0,0 +1,22 @@
import Image from 'next/image';
import Toggle from './Toggle';
const Header = () => {
return (
<div className='flex justify-between p-4'>
<header className='flex items-center p-4'>
<Image
src='/images/knative-logo.png'
alt='Knative BookStore Logo'
width={50}
height={50}
className='mr-2'
/>
<h1 className='text-3xl font-bold'>Knative BookStore</h1>
</header>
<Toggle />
</div>
);
};
export default Header;

View File

@ -0,0 +1,26 @@
'use client';
import React from 'react';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
const Toggle = () => {
const { systemTheme, theme, setTheme } = useTheme();
useEffect(() => {
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
setTheme(systemPreference);
}, []);
return (
<button
onClick={() => (theme == 'dark' ? setTheme('light') : setTheme('dark'))}
className='bg-gray-800 dark:bg-gray-50 hover:bg-gray-600 dark:hover:bg-gray-300 transition-all duration-100 text-white dark:text-gray-800 px-6 py-1 text-xl md:text-2xl rounded-md'
>
Mode
</button>
);
};
export default Toggle;

View File

@ -0,0 +1,37 @@
'use client';
import Header from '../components/Header';
import BookDetail from '../components/BookDetail';
import CommentForm from '../components/CommentForm';
import CommentList from '../components/CommentList';
import { ThemeProvider } from 'next-themes';
export default function Main() {
/* Example Book object */
const book = {
img: '/images/Bookcover.jpg',
title: 'Building serverless applications on Knative',
author: 'Evan Anderson',
ISBN: '978-1098142070',
publisher: 'Oreilly & Associates Inc',
publishedDate: 'December 19, 2023',
description:
'A Guide to Designing and Writing Serverless Cloud Application',
price: '$49',
};
return (
<div>
<Header />
<main className='container mx-auto my-8'>
<BookDetail book={book} />
<div className="max-w-4xl mx-auto">
<CommentForm />
<p className="text-xl font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-200">
Comments
</p>
<CommentList />
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.1.3",
"@nextui-org/react": "^2.2.10",
"framer-motion": "^11.0.22",
"next": "14.1.4",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.4",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,24 @@
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./client/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./client/components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
fontFamily: {
sans: ['Poppins', 'sans-serif'],
},
},
},
darkMode: 'class',
plugins: [],
};
export default config;

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}