Style part page
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
18
web/src/components/Breadcrumb/Breadcrumb.js
Normal file
18
web/src/components/Breadcrumb/Breadcrumb.js
Normal 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
|
||||
7
web/src/components/Breadcrumb/Breadcrumb.stories.js
Normal file
7
web/src/components/Breadcrumb/Breadcrumb.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Breadcrumb from './Breadcrumb'
|
||||
|
||||
export const generated = () => {
|
||||
return <Breadcrumb />
|
||||
}
|
||||
|
||||
export default { title: 'Components/Breadcrumb' }
|
||||
11
web/src/components/Breadcrumb/Breadcrumb.test.js
Normal file
11
web/src/components/Breadcrumb/Breadcrumb.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import Breadcrumb from './Breadcrumb'
|
||||
|
||||
describe('Breadcrumb', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<Breadcrumb />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
110
web/src/components/Part2Cell/Part2Cell.js
Normal file
110
web/src/components/Part2Cell/Part2Cell.js
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
6
web/src/components/Part2Cell/Part2Cell.mock.js
Normal file
6
web/src/components/Part2Cell/Part2Cell.mock.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Define your own mock data here:
|
||||
export const standard = (/* vars, { ctx, req } */) => ({
|
||||
part2: {
|
||||
id: 42,
|
||||
},
|
||||
})
|
||||
20
web/src/components/Part2Cell/Part2Cell.stories.js
Normal file
20
web/src/components/Part2Cell/Part2Cell.stories.js
Normal 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' }
|
||||
26
web/src/components/Part2Cell/Part2Cell.test.js
Normal file
26
web/src/components/Part2Cell/Part2Cell.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
12
web/src/pages/Part2Page/Part2Page.js
Normal file
12
web/src/pages/Part2Page/Part2Page.js
Normal 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
|
||||
7
web/src/pages/Part2Page/Part2Page.stories.js
Normal file
7
web/src/pages/Part2Page/Part2Page.stories.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Part2Page from './Part2Page'
|
||||
|
||||
export const generated = () => {
|
||||
return <Part2Page />
|
||||
}
|
||||
|
||||
export default { title: 'Pages/Part2Page' }
|
||||
11
web/src/pages/Part2Page/Part2Page.test.js
Normal file
11
web/src/pages/Part2Page/Part2Page.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { render } from '@redwoodjs/testing'
|
||||
|
||||
import Part2Page from './Part2Page'
|
||||
|
||||
describe('Part2Page', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<Part2Page />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user