Style part page

This commit is contained in:
Kurt Hutten
2020-11-07 16:38:48 +11:00
parent 606cf8eae8
commit 1bdc836b66
18 changed files with 347 additions and 61 deletions

View File

@@ -17,25 +17,34 @@ const Routes = () => {
<Route notfound page={NotFoundPage} />
<Route path="/u/{userName}" page={User2Page} name="user2" />
<Route path="/u/{userName}/{partTitle}" page={Part2Page} name="part2" />
{/* <Route path="/u/{userName}/{partTitle}/ide" page={Part2Page} name="part2" /> */}
{/* Ownership enforced routes */}
<Route path="/u/{userName}/edit" page={EditUser2Page} name="editUser2" />
{/* <Route path="/u/{userName}/{partTitle}/edit" page={Part2Page} name="part2" /> */}
{/* End ownership enforced routes */}
{/* GENERATED ROUTES BELOW, probably going to clean these up and delete most of them, but the CRUD functionality is useful for now */}
<Route path="/part-reactions/new" page={NewPartReactionPage} name="newPartReaction" />
<Route path="/part-reactions/{id}/edit" page={EditPartReactionPage} name="editPartReaction" />
<Route path="/part-reactions/{id}" page={PartReactionPage} name="partReaction" />
<Route path="/part-reactions" page={PartReactionsPage} name="partReactions" />
<Route path="/parts/new" page={NewPartPage} name="newPart" />
<Route path="/parts/{id}/edit" page={EditPartPage} name="editPart" />
<Route path="/parts/{id}" page={PartPage} name="part" />
<Route path="/parts" page={PartsPage} name="parts" />
<Route path="/comments/new" page={NewCommentPage} name="newComment" />
<Route path="/comments/{id}/edit" page={EditCommentPage} name="editComment" />
<Route path="/comments/{id}" page={CommentPage} name="comment" />
<Route path="/comments" page={CommentsPage} name="comments" />
<Route path="/users/new" page={NewUserPage} name="newUser" />
<Route path="/users/{id}/edit" page={EditUserPage} name="editUser" />
<Route path="/users/{id}" page={UserPage} name="user" />
<Route path="/users" page={UsersPage} name="users" />
{/* All private by default for safety and because the routes that are left after clean up will probably be admin pages */}
<Private unauthenticated="home" role="admin">
<Route path="/part-reactions/new" page={NewPartReactionPage} name="newPartReaction" />
<Route path="/part-reactions/{id}/edit" page={EditPartReactionPage} name="editPartReaction" />
<Route path="/part-reactions/{id}" page={PartReactionPage} name="partReaction" />
<Route path="/part-reactions" page={PartReactionsPage} name="partReactions" />
<Route path="/parts/new" page={NewPartPage} name="newPart" />
<Route path="/parts/{id}/edit" page={EditPartPage} name="editPart" />
<Route path="/parts/{id}" page={PartPage} name="part" />
<Route path="/parts" page={PartsPage} name="parts" />
<Route path="/comments/new" page={NewCommentPage} name="newComment" />
<Route path="/comments/{id}/edit" page={EditCommentPage} name="editComment" />
<Route path="/comments/{id}" page={CommentPage} name="comment" />
<Route path="/comments" page={CommentsPage} name="comments" />
<Route path="/users/new" page={NewUserPage} name="newUser" />
<Route path="/users/{id}/edit" page={EditUserPage} name="editUser" />
<Route path="/users/{id}" page={UserPage} name="user" />
<Route path="/users" page={UsersPage} name="users" />
</Private>
</Router>
)
}

View File

@@ -0,0 +1,18 @@
import { getActiveClasses } from "get-active-classes"
const Breadcrumb = ({ userName, partTitle, className }) => {
return (
<h3 className={getActiveClasses("text-2xl font-roboto", className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">.</div>
<span className="text-gray-500">
{userName}
</span>
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20" >.</div>
<span className="text-indigo-800">
{partTitle}
</span>
</h3>
)
}
export default Breadcrumb

View File

@@ -0,0 +1,7 @@
import Breadcrumb from './Breadcrumb'
export const generated = () => {
return <Breadcrumb />
}
export default { title: 'Components/Breadcrumb' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import Breadcrumb from './Breadcrumb'
describe('Breadcrumb', () => {
it('renders successfully', () => {
expect(() => {
render(<Breadcrumb />)
}).not.toThrow()
})
})

View File

@@ -1,16 +1,19 @@
import { getActiveClasses } from 'get-active-classes'
import Svg from 'src/components/Svg'
const Button = ({onClick, iconName, children}) => {
const Button = ({onClick, iconName, children, className, shouldAnimateHover}) => {
return (
<div>
<button
className="flex items-center bg-indigo-200 bg-opacity-50 rounded-xl p-2 px-6 text-indigo-600"
className={getActiveClasses(
"flex items-center bg-opacity-50 rounded-xl p-2 px-6 text-indigo-600",
{'mx-px transform hover:-translate-y-px transition-all duration-150': shouldAnimateHover},
className
)}
onClick={onClick}
>
{children}
<Svg className="w-6 ml-4" name={iconName} />
</button>
</div>
)
}

View File

@@ -1,12 +1,19 @@
import { useState } from 'react'
import Fab from '@material-ui/core/Fab'
import IconButton from '@material-ui/core/IconButton'
import { getActiveClasses } from "get-active-classes"
import Popover from '@material-ui/core/Popover'
import Svg from 'src/components/Svg'
const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
const emojiMenu = ['❤️', '👍', '😄', '🙌']
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
const noEmotes =[{
emoji: '❤️',
count: 0,
}]
const EmojiReaction = ({ emotes, callback = () => {} }) => {
const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'}
const EmojiReaction = ({ emotes, onEmote = () => {}, className }) => {
const [isOpen, setIsOpen] = useState(false)
const [anchorEl, setAnchorEl] = useState(null)
const [popoverId, setPopoverId] = useState(undefined)
@@ -32,45 +39,63 @@ const EmojiReaction = ({ emotes, callback = () => {} }) => {
}
const handleEmojiClick = (emoji) => {
callback(emoji)
onEmote(emoji)
closePopover()
}
return [
<div className="flex justify-between">
<Fab size="medium" variant="round" aria-describedby={popoverId} onClick={togglePopover}>
<div className="bg-gray-200 border-2 m-px border-gray-300 text-gray-500 absolute inset-0 rounded-full flex justify-center items-center">
<Svg name="dots-vertical" />
return (
<>
<div className={getActiveClasses("h-10 relative overflow-hidden py-4", className)}>
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
<div className="h-8 w-8 relative" aria-describedby={popoverId} onClick={togglePopover}>
<button
className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150"
>
<Svg className="h-8 w-8 pt-px mt-px text-gray-500" name="dots-vertical" />
</button>
</div>
</div>
</Fab>
<div>
{emotes.map((emote, i) => (
<IconButton key={`${emote.emoji}--${i}`} onClick={() => handleEmojiClick(emote.emoji)}>
{emote.emoji} <span>{emote.count}</span>
</IconButton>
))}
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
<span
className="rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150"
style={textShadow}
key={`${emote.emoji}--${i}`}
onClick={() => handleEmojiClick(emote.emoji)}
>
<span className="text-lg pr-1">{emote.emoji}</span><span className="text-sm font-ropa-sans">{emote.count}</span>
</span>
))}
</div>
</div>
</div>,
<Popover
id={popoverId}
open={isOpen}
anchorEl={anchorEl}
onClose={closePopover}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
{emojiMenu.map((emoji, i) => (
<IconButton key={`${emoji}-${i}}`} onClick={() => handleEmojiClick(emoji)}>{emoji}</IconButton>
))}
</Popover>,
]
<Popover
id={popoverId}
open={isOpen}
anchorEl={anchorEl}
onClose={closePopover}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<div className="py-2 mt-2">
{emojiMenu.map((emoji, i) => (
<button
className="p-2"
style={textShadow}
key={`${emoji}-${i}}`}
onClick={() => handleEmojiClick(emoji)}
>{emoji}</button>
))}
</div>
</Popover>
</>
)
}
export default EmojiReaction

View File

@@ -0,0 +1,110 @@
import Editor from "rich-markdown-editor";
// import Part from 'src/components/Part'
import ImageUploader from 'src/components/ImageUploader'
import Breadcrumb from 'src/components/Breadcrumb'
import EmojiReaction from 'src/components/EmojiReaction'
import Button from 'src/components/Button'
export const QUERY = gql`
query FIND_PART_BY_USERNAME_TITLE($userName: String!, $partTitle: String!) {
userPart: userName(userName: $userName) {
name
userName
bio
image
id
Part(partTitle: $partTitle) {
id
title
description
code
mainImage
createdAt
updatedAt
userId
}
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ userPart, variables }) => {
return (
<>
<div className="grid mt-20 gap-8" style={{gridTemplateColumns: 'auto 12rem minmax(12rem, 42rem) 10rem auto'}}>
{/* Side column */}
<aside className="col-start-2">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={userPart.image === 'abc' ? '': userPart.image}
/>
<h4 className="text-indigo-800 text-xl underline text-right py-4">{userPart?.name}</h4>
<div className="h-px bg-indigo-200 mb-4" />
{/* TODO hook up to emoji data properly */}
<EmojiReaction
// emotes={[{emoji: '❤️',count: 3},{emoji: '😁',count: 2},{emoji: '😜',count: 2},{emoji: '🤩',count: 2},{emoji: '🤣',count: 2},{emoji: '🙌',count: 2},{emoji: '🚀',count: 2}]}
emotes={[{emoji: '❤️',count: 3},{emoji: '😁',count: 2}]}
// emotes={[]}
onEmote={() => {}}
/>
<Button
className="mt-6 ml-auto hover:shadow-lg bg-gradient-to-r from-transparent to-indigo-100"
shouldAnimateHover
iconName="plus"
onClick={() => {}}
>
Comments 11
</Button>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200"
shouldAnimateHover
iconName="plus"
onClick={() => {}}
>
Open IDE
</Button>
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200"
shouldAnimateHover
iconName="pencil"
onClick={() => {}}
>
Edit Part
</Button>
</aside>
{/* main project center column */}
<section className="col-start-3">
<Breadcrumb className="mb-8" userName={userPart.userName} partTitle={userPart?.Part?.title}/>
{ userPart?.Part?.mainImage && <ImageUploader
className="rounded-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={() => {}}
aspectRatio={16/9}
imageUrl={userPart?.Part?.mainImage}
/>}
<div name="description" className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-8 min-h-md">
<Editor
defaultValue={userPart?.Part?.description || ''}
readOnly
// readOnly={!isEditable}
// onChange={(bioFn) => setInput({
// ...input,
// bio: bioFn(),
// })}
/>
</div>
</section>
</div>
</>
)
}

View File

@@ -0,0 +1,6 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
part2: {
id: 42,
},
})

View File

@@ -0,0 +1,20 @@
import { Loading, Empty, Failure, Success } from './Part2Cell'
import { standard } from './Part2Cell.mock'
export const loading = () => {
return Loading ? <Loading /> : null
}
export const empty = () => {
return Empty ? <Empty /> : null
}
export const failure = () => {
return Failure ? <Failure error={new Error('Oh no')} /> : null
}
export const success = () => {
return Success ? <Success {...standard()} /> : null
}
export default { title: 'Cells/Part2Cell' }

View File

@@ -0,0 +1,26 @@
import { render, screen } from '@redwoodjs/testing'
import { Loading, Empty, Failure, Success } from './Part2Cell'
import { standard } from './Part2Cell.mock'
describe('Part2Cell', () => {
test('Loading renders successfully', () => {
render(<Loading />)
// Use screen.debug() to see output
expect(screen.getByText('Loading...')).toBeInTheDocument()
})
test('Empty renders successfully', async () => {
render(<Empty />)
expect(screen.getByText('Empty')).toBeInTheDocument()
})
test('Failure renders successfully', async () => {
render(<Failure error={new Error('Oh no')} />)
expect(screen.getByText(/Oh no/i)).toBeInTheDocument()
})
test('Success renders successfully', async () => {
render(<Success part2={standard().part2} />)
expect(screen.getByText(/42/i)).toBeInTheDocument()
})
})

View File

@@ -45,9 +45,9 @@ const UserProfile = ({user, isEditable, loading, onSave, error}) => {
...textFields,
})} isEditable={isEditable}/>
{isEditable ?
<Button iconName="plus" onClick={() => onSave(user.userName, input)}>Save Profile</Button> : // TODO replace pencil with a save icon
<Button className="bg-indigo-200" iconName="plus" onClick={() => onSave(user.userName, input)}>Save Profile</Button> : // TODO replace pencil with a save icon
canEdit ?
<Button iconName="pencil" onClick={() => navigate(routes.editUser2({userName: user.userName}))}>Edit Profile</Button>:
<Button className="bg-indigo-200" iconName="pencil" onClick={() => navigate(routes.editUser2({userName: user.userName}))}>Edit Profile</Button>:
null
}
</div>

View File

@@ -0,0 +1,12 @@
import MainLayout from 'src/layouts/MainLayout'
import Part2Cell from 'src/components/Part2Cell'
const Part2Page = ({userName, partTitle}) => {
return (
<MainLayout>
<Part2Cell userName={userName} partTitle={partTitle}/>
</MainLayout>
)
}
export default Part2Page

View File

@@ -0,0 +1,7 @@
import Part2Page from './Part2Page'
export const generated = () => {
return <Part2Page />
}
export default { title: 'Pages/Part2Page' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import Part2Page from './Part2Page'
describe('Part2Page', () => {
it('renders successfully', () => {
expect(() => {
render(<Part2Page />)
}).not.toThrow()
})
})