Merge pull request #92 from Irev-Dev/lint-whole-project
Lint project
This commit was merged in pull request #92.
This commit is contained in:
@@ -68,11 +68,12 @@ export const handler = async (req, _context) => {
|
|||||||
const isUnique = !(await db.user.findOne({
|
const isUnique = !(await db.user.findOne({
|
||||||
where: { userName: seed },
|
where: { userName: seed },
|
||||||
}))
|
}))
|
||||||
if(isUnique) {
|
if (isUnique) {
|
||||||
return seed
|
return seed
|
||||||
}
|
}
|
||||||
count += 1
|
count += 1
|
||||||
const newSeed = count === 1 ? `${seed}_${count}` : seed.slice(0,-1) + count
|
const newSeed =
|
||||||
|
count === 1 ? `${seed}_${count}` : seed.slice(0, -1) + count
|
||||||
return generateUniqueUserName(newSeed, count)
|
return generateUniqueUserName(newSeed, count)
|
||||||
}
|
}
|
||||||
const userNameSeed = enforceAlphaNumeric(email.split('@')[0])
|
const userNameSeed = enforceAlphaNumeric(email.split('@')[0])
|
||||||
@@ -83,7 +84,7 @@ export const handler = async (req, _context) => {
|
|||||||
name: user.user_metadata && user.user_metadata.full_name,
|
name: user.user_metadata && user.user_metadata.full_name,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
}
|
}
|
||||||
await createUserInsecure({input})
|
await createUserInsecure({ input })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const schema = gql`
|
|||||||
type Query {
|
type Query {
|
||||||
parts: [Part!]!
|
parts: [Part!]!
|
||||||
part(id: String!): Part
|
part(id: String!): Part
|
||||||
partByUserAndTitle(userName: String! partTitle: String!): Part
|
partByUserAndTitle(userName: String!, partTitle: String!): Part
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreatePartInput {
|
input CreatePartInput {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const requireAuth = ({ role } = {}) => {
|
|||||||
throw new ForbiddenError("You don't have access to do that.")
|
throw new ForbiddenError("You don't have access to do that.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
|
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
|
||||||
throw new ForbiddenError("That's a local admin ONLY.")
|
throw new ForbiddenError("That's a local admin ONLY.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,37 +7,38 @@ export const requireOwnership = async ({ userId, userName, partId } = {}) => {
|
|||||||
if (!context.currentUser) {
|
if (!context.currentUser) {
|
||||||
throw new AuthenticationError("You don't have permission to do that.")
|
throw new AuthenticationError("You don't have permission to do that.")
|
||||||
}
|
}
|
||||||
if(!userId && !userName && !partId) {
|
if (!userId && !userName && !partId) {
|
||||||
throw new ForbiddenError("You don't have access to do that.")
|
throw new ForbiddenError("You don't have access to do that.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(context.currentUser.roles?.includes('admin')) {
|
if (context.currentUser.roles?.includes('admin')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const netlifyUserId = context.currentUser?.sub
|
const netlifyUserId = context.currentUser?.sub
|
||||||
if(userId && userId !== netlifyUserId) {
|
if (userId && userId !== netlifyUserId) {
|
||||||
throw new ForbiddenError("You don't own this resource.")
|
throw new ForbiddenError("You don't own this resource.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(userName) {
|
if (userName) {
|
||||||
const user = await db.user.findOne({
|
const user = await db.user.findOne({
|
||||||
where: { userName },
|
where: { userName },
|
||||||
})
|
})
|
||||||
|
|
||||||
if(!user || user.id !== netlifyUserId) {
|
if (!user || user.id !== netlifyUserId) {
|
||||||
throw new ForbiddenError("You don't own this resource.")
|
throw new ForbiddenError("You don't own this resource.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(partId) {
|
if (partId) {
|
||||||
const user = await db.part.findOne({
|
const user = await db.part
|
||||||
where: { id: partId },
|
.findOne({
|
||||||
}).user()
|
where: { id: partId },
|
||||||
|
})
|
||||||
|
.user()
|
||||||
|
|
||||||
if(!user || user.id !== netlifyUserId) {
|
if (!user || user.id !== netlifyUserId) {
|
||||||
throw new ForbiddenError("You don't own this resource.")
|
throw new ForbiddenError("You don't own this resource.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export const foreignKeyReplacement = (input) => {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enforceAlphaNumeric = (string) => string.replace(/([^a-zA-Z\d_:])/g, '-')
|
export const enforceAlphaNumeric = (string) =>
|
||||||
|
string.replace(/([^a-zA-Z\d_:])/g, '-')
|
||||||
|
|||||||
@@ -18,20 +18,24 @@ export const partReaction = ({ id }) => {
|
|||||||
export const togglePartReaction = async ({ input }) => {
|
export const togglePartReaction = async ({ input }) => {
|
||||||
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
// if write fails emote_userId_partId @@unique constraint, then delete it instead
|
||||||
requireAuth()
|
requireAuth()
|
||||||
await requireOwnership({userId: input?.userId})
|
await requireOwnership({ userId: input?.userId })
|
||||||
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
|
const legalReactions = ['❤️', '👍', '😄', '🙌'] // TODO figure out a way of sharing code between FE and BE, so this is consistent with web/src/components/EmojiReaction/EmojiReaction.js
|
||||||
if(!legalReactions.includes(input.emote)) {
|
if (!legalReactions.includes(input.emote)) {
|
||||||
throw new UserInputError(`You can't react with '${input.emote}', only the following are allowed: ${legalReactions.join(', ')}`)
|
throw new UserInputError(
|
||||||
|
`You can't react with '${
|
||||||
|
input.emote
|
||||||
|
}', only the following are allowed: ${legalReactions.join(', ')}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
let dbPromise
|
let dbPromise
|
||||||
const inputClone = {...input} // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
|
const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
|
||||||
try{
|
try {
|
||||||
dbPromise = await db.partReaction.create({
|
dbPromise = await db.partReaction.create({
|
||||||
data: foreignKeyReplacement(input),
|
data: foreignKeyReplacement(input),
|
||||||
})
|
})
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
dbPromise = db.partReaction.delete({
|
dbPromise = db.partReaction.delete({
|
||||||
where: { emote_userId_partId: inputClone},
|
where: { emote_userId_partId: inputClone },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return dbPromise
|
return dbPromise
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
import { foreignKeyReplacement, enforceAlphaNumeric } from 'src/services/helpers'
|
import {
|
||||||
|
foreignKeyReplacement,
|
||||||
|
enforceAlphaNumeric,
|
||||||
|
} from 'src/services/helpers'
|
||||||
import { requireAuth } from 'src/lib/auth'
|
import { requireAuth } from 'src/lib/auth'
|
||||||
import { requireOwnership } from 'src/lib/owner'
|
import { requireOwnership } from 'src/lib/owner'
|
||||||
|
|
||||||
@@ -15,15 +18,15 @@ export const part = ({ id }) => {
|
|||||||
export const partByUserAndTitle = async ({ userName, partTitle }) => {
|
export const partByUserAndTitle = async ({ userName, partTitle }) => {
|
||||||
const user = await db.user.findOne({
|
const user = await db.user.findOne({
|
||||||
where: {
|
where: {
|
||||||
userName
|
userName,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
return db.part.findOne({
|
return db.part.findOne({
|
||||||
where: {
|
where: {
|
||||||
title_userId: {
|
title_userId: {
|
||||||
title: partTitle,
|
title: partTitle,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,8 +40,8 @@ export const createPart = async ({ input }) => {
|
|||||||
|
|
||||||
export const updatePart = async ({ id, input }) => {
|
export const updatePart = async ({ id, input }) => {
|
||||||
requireAuth()
|
requireAuth()
|
||||||
await requireOwnership({partId: id})
|
await requireOwnership({ partId: id })
|
||||||
if(input.title) {
|
if (input.title) {
|
||||||
input.title = enforceAlphaNumeric(input.title)
|
input.title = enforceAlphaNumeric(input.title)
|
||||||
}
|
}
|
||||||
return db.part.update({
|
return db.part.update({
|
||||||
@@ -59,5 +62,7 @@ export const Part = {
|
|||||||
Comment: (_obj, { root }) =>
|
Comment: (_obj, { root }) =>
|
||||||
db.part.findOne({ where: { id: root.id } }).Comment(),
|
db.part.findOne({ where: { id: root.id } }).Comment(),
|
||||||
Reaction: (_obj, { root }) =>
|
Reaction: (_obj, { root }) =>
|
||||||
db.part.findOne({ where: { id: root.id } }).Reaction({where: {userId: _obj.userId}}),
|
db.part
|
||||||
|
.findOne({ where: { id: root.id } })
|
||||||
|
.Reaction({ where: { userId: _obj.userId } }),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const userName = ({ userName }) => {
|
|||||||
|
|
||||||
export const createUser = ({ input }) => {
|
export const createUser = ({ input }) => {
|
||||||
requireAuth({ role: 'admin' })
|
requireAuth({ role: 'admin' })
|
||||||
createUserInsecure({input})
|
createUserInsecure({ input })
|
||||||
}
|
}
|
||||||
export const createUserInsecure = ({ input }) => {
|
export const createUserInsecure = ({ input }) => {
|
||||||
return db.user.create({
|
return db.user.create({
|
||||||
@@ -41,12 +41,15 @@ export const updateUser = ({ id, input }) => {
|
|||||||
|
|
||||||
export const updateUserByUserName = async ({ userName, input }) => {
|
export const updateUserByUserName = async ({ userName, input }) => {
|
||||||
requireAuth()
|
requireAuth()
|
||||||
await requireOwnership({userName})
|
await requireOwnership({ userName })
|
||||||
if(input.userName) {
|
if (input.userName) {
|
||||||
input.userName = enforceAlphaNumeric(input.userName)
|
input.userName = enforceAlphaNumeric(input.userName)
|
||||||
}
|
}
|
||||||
if(input.userName && ['new', 'edit', 'update'].includes(input.userName)) { //TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
if (input.userName && ['new', 'edit', 'update'].includes(input.userName)) {
|
||||||
throw new UserInputError(`You've tried to used a protected word as you userName, try something other than `)
|
//TODO complete this and use a regexp so that it's not case sensitive, don't want someone with the userName eDiT
|
||||||
|
throw new UserInputError(
|
||||||
|
`You've tried to used a protected word as you userName, try something other than `
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return db.user.update({
|
return db.user.update({
|
||||||
data: input,
|
data: input,
|
||||||
@@ -63,10 +66,16 @@ export const deleteUser = ({ id }) => {
|
|||||||
|
|
||||||
export const User = {
|
export const User = {
|
||||||
Parts: (_obj, { root }) => db.user.findOne({ where: { id: root.id } }).Part(),
|
Parts: (_obj, { root }) => db.user.findOne({ where: { id: root.id } }).Part(),
|
||||||
Part: (_obj, { root, ...rest }) => _obj.partTitle && db.part.findOne({where: { title_userId: {
|
Part: (_obj, { root, ...rest }) =>
|
||||||
title: _obj.partTitle,
|
_obj.partTitle &&
|
||||||
userId: root.id,
|
db.part.findOne({
|
||||||
}}}),
|
where: {
|
||||||
|
title_userId: {
|
||||||
|
title: _obj.partTitle,
|
||||||
|
userId: root.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
Reaction: (_obj, { root }) =>
|
Reaction: (_obj, { root }) =>
|
||||||
db.user.findOne({ where: { id: root.id } }).Reaction(),
|
db.user.findOne({ where: { id: root.id } }).Reaction(),
|
||||||
Comment: (_obj, { root }) =>
|
Comment: (_obj, { root }) =>
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -14,15 +14,7 @@
|
|||||||
"@redwoodjs/core": "^0.19.2"
|
"@redwoodjs/core": "^0.19.2"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@redwoodjs/eslint-config",
|
"extends": "@redwoodjs/eslint-config"
|
||||||
"workingDirectories": [
|
|
||||||
"./api",
|
|
||||||
"./web/src/components",
|
|
||||||
"./web/src/layouts",
|
|
||||||
"./web/src/pages",
|
|
||||||
"./web/src/index.js",
|
|
||||||
"./web/src/Routes.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12",
|
"node": ">=12",
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { getActiveClasses } from "get-active-classes"
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
|
||||||
import InputText from 'src/components/InputText'
|
import InputText from 'src/components/InputText'
|
||||||
|
|
||||||
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
|
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
|
||||||
return (
|
return (
|
||||||
<h3 className={getActiveClasses("text-2xl font-roboto", className)}>
|
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
|
||||||
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">.</div>
|
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
|
||||||
<span className={getActiveClasses({"text-gray-500": !onPartTitleChange, 'text-gray-400': onPartTitleChange})}>
|
.
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={getActiveClasses({
|
||||||
|
'text-gray-500': !onPartTitleChange,
|
||||||
|
'text-gray-400': onPartTitleChange,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{userName}
|
{userName}
|
||||||
</span>
|
</span>
|
||||||
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20" >.</div>
|
<div className="w-1 inline-block bg-gray-400 text-gray-400 mx-3 transform -skew-x-20">
|
||||||
|
.
|
||||||
|
</div>
|
||||||
<InputText
|
<InputText
|
||||||
value={partTitle}
|
value={partTitle}
|
||||||
onChange={onPartTitleChange}
|
onChange={onPartTitleChange}
|
||||||
isEditable={onPartTitleChange}
|
isEditable={onPartTitleChange}
|
||||||
className={getActiveClasses("text-indigo-800 text-2xl", {'-ml-2': !onPartTitleChange})}
|
className={getActiveClasses('text-indigo-800 text-2xl', {
|
||||||
|
'-ml-2': !onPartTitleChange,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
import { getActiveClasses } from 'get-active-classes'
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
import Svg from 'src/components/Svg'
|
import Svg from 'src/components/Svg'
|
||||||
|
|
||||||
const Button = ({onClick, iconName, children, className, shouldAnimateHover, disabled}) => {
|
const Button = ({
|
||||||
|
onClick,
|
||||||
|
iconName,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
shouldAnimateHover,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={getActiveClasses(
|
className={getActiveClasses(
|
||||||
"flex items-center bg-opacity-50 rounded-xl p-2 px-6 text-indigo-600",
|
'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 && !disabled},
|
{
|
||||||
className,
|
'mx-px transform hover:-translate-y-px transition-all duration-150':
|
||||||
{"bg-gray-300 shadow-none hover:shadow-none": disabled},
|
shouldAnimateHover && !disabled,
|
||||||
)}
|
},
|
||||||
onClick={onClick}
|
className,
|
||||||
>
|
{ 'bg-gray-300 shadow-none hover:shadow-none': disabled }
|
||||||
{children}
|
)}
|
||||||
<Svg className="w-6 ml-4" name={iconName} />
|
onClick={onClick}
|
||||||
</button>
|
>
|
||||||
|
{children}
|
||||||
|
<Svg className="w-6 ml-4" name={iconName} />
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
|
|
||||||
export const generated = () => {
|
export const generated = () => {
|
||||||
return <>
|
return (
|
||||||
button with icon
|
<>
|
||||||
<Button>click Me </Button>
|
button with icon
|
||||||
</>
|
<Button>click Me </Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { title: 'Components/Button' }
|
export default { title: 'Components/Button' }
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
|||||||
export const Success = ({ user }) => {
|
export const Success = ({ user }) => {
|
||||||
const { addMessage } = useFlash()
|
const { addMessage } = useFlash()
|
||||||
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
|
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
|
||||||
onCompleted: ({updateUserByUserName}) => {
|
onCompleted: ({ updateUserByUserName }) => {
|
||||||
navigate(routes.user2({userName: updateUserByUserName.userName}))
|
navigate(routes.user2({ userName: updateUserByUserName.userName }))
|
||||||
addMessage('User updated.', { classes: 'rw-flash-success' })
|
addMessage('User updated.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -45,11 +45,13 @@ export const Success = ({ user }) => {
|
|||||||
updateUser({ variables: { userName, input } })
|
updateUser({ variables: { userName, input } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return <UserProfile
|
return (
|
||||||
user={user}
|
<UserProfile
|
||||||
onSave={onSave}
|
user={user}
|
||||||
loading={loading}
|
onSave={onSave}
|
||||||
error={error}
|
loading={loading}
|
||||||
isEditable
|
error={error}
|
||||||
/>
|
isEditable
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { getActiveClasses } from "get-active-classes"
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
import Popover from '@material-ui/core/Popover'
|
import Popover from '@material-ui/core/Popover'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
|
|
||||||
@@ -7,14 +7,21 @@ import Svg from 'src/components/Svg'
|
|||||||
|
|
||||||
const emojiMenu = ['❤️', '👍', '😄', '🙌']
|
const emojiMenu = ['❤️', '👍', '😄', '🙌']
|
||||||
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
|
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
|
||||||
const noEmotes =[{
|
const noEmotes = [
|
||||||
emoji: '❤️',
|
{
|
||||||
count: 0,
|
emoji: '❤️',
|
||||||
}]
|
count: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const textShadow = {textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'}
|
const textShadow = { textShadow: '0 4px 6px rgba(0, 0, 0, 0.3)' }
|
||||||
|
|
||||||
const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) => {
|
const EmojiReaction = ({
|
||||||
|
emotes,
|
||||||
|
userEmotes,
|
||||||
|
onEmote = () => {},
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
@@ -48,29 +55,40 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={getActiveClasses("h-10 relative overflow-hidden py-4", className)}>
|
<div
|
||||||
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
|
className={getActiveClasses(
|
||||||
<div className="h-8 w-8 relative" aria-describedby={popoverId} onClick={togglePopover}>
|
'h-10 relative overflow-hidden py-4',
|
||||||
<button
|
className
|
||||||
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" />
|
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
|
||||||
</button>
|
<div
|
||||||
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
|
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
|
||||||
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
|
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
|
||||||
<span
|
<span
|
||||||
className={getActiveClasses(
|
className={getActiveClasses(
|
||||||
"rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400",
|
'rounded-full tracking-wide hover:bg-indigo-100 p-1 mx-px transform hover:-translate-y-px transition-all duration-150 border-indigo-400',
|
||||||
{'border': currentUser && userEmotes?.includes(emote.emoji)}
|
{ border: currentUser && userEmotes?.includes(emote.emoji) }
|
||||||
)}
|
)}
|
||||||
style={textShadow}
|
style={textShadow}
|
||||||
key={`${emote.emoji}--${i}`}
|
key={`${emote.emoji}--${i}`}
|
||||||
onClick={() => handleEmojiClick(emote.emoji)}
|
onClick={() => handleEmojiClick(emote.emoji)}
|
||||||
>
|
>
|
||||||
<span className="text-lg pr-1">{emote.emoji}</span><span className="text-sm font-ropa-sans">{emote.count}</span>
|
<span className="text-lg pr-1">{emote.emoji}</span>
|
||||||
|
<span className="text-sm font-ropa-sans">{emote.count}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +114,9 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) =>
|
|||||||
style={textShadow}
|
style={textShadow}
|
||||||
key={`${emoji}-${i}}`}
|
key={`${emoji}-${i}}`}
|
||||||
onClick={() => handleEmojiClick(emoji)}
|
onClick={() => handleEmojiClick(emoji)}
|
||||||
>{emoji}</button>
|
>
|
||||||
|
{emoji}
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ export const Success = ({ part }) => {
|
|||||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
console.log({updatePart})
|
console.log({ updatePart })
|
||||||
|
|
||||||
|
|
||||||
const saveCode = (input, id) => {
|
const saveCode = (input, id) => {
|
||||||
console.log(id, input, 'wowow')
|
console.log(id, input, 'wowow')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from 'react'
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from 'react-dropzone'
|
||||||
import Button from "@material-ui/core/Button";
|
import Button from '@material-ui/core/Button'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ReactCrop from 'react-image-crop'
|
import ReactCrop from 'react-image-crop'
|
||||||
import { Dialog } from '@material-ui/core'
|
import { Dialog } from '@material-ui/core'
|
||||||
@@ -8,17 +8,17 @@ import { Image as CloudinaryImage } from 'cloudinary-react'
|
|||||||
import 'react-image-crop/dist/ReactCrop.css'
|
import 'react-image-crop/dist/ReactCrop.css'
|
||||||
import Svg from 'src/components/Svg/Svg.js'
|
import Svg from 'src/components/Svg/Svg.js'
|
||||||
|
|
||||||
const CLOUDINARY_UPLOAD_PRESET = "CadHub_project_images";
|
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
|
||||||
const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload";
|
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
|
||||||
|
|
||||||
export default function ImageUploader({
|
export default function ImageUploader({
|
||||||
onImageUpload,
|
onImageUpload,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
className,
|
className,
|
||||||
isEditable,
|
isEditable,
|
||||||
width=600
|
width = 600,
|
||||||
}) {
|
}) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [file, setFile] = useState()
|
const [file, setFile] = useState()
|
||||||
const [cloudinaryId, setCloudinaryId] = useState(imageUrl)
|
const [cloudinaryId, setCloudinaryId] = useState(imageUrl)
|
||||||
@@ -27,17 +27,17 @@ export default function ImageUploader({
|
|||||||
aspect: aspectRatio,
|
aspect: aspectRatio,
|
||||||
unit: '%',
|
unit: '%',
|
||||||
width: 100,
|
width: 100,
|
||||||
});
|
})
|
||||||
async function handleImageUpload() {
|
async function handleImageUpload() {
|
||||||
const croppedFile = await getCroppedImg(imageObj, crop, 'avatar')
|
const croppedFile = await getCroppedImg(imageObj, crop, 'avatar')
|
||||||
const imageData = new FormData();
|
const imageData = new FormData()
|
||||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
|
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
|
||||||
imageData.append('file', croppedFile);
|
imageData.append('file', croppedFile)
|
||||||
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
|
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
|
||||||
try {
|
try {
|
||||||
const { data } = await upload
|
const { data } = await upload
|
||||||
if (data && data.public_id !== "") {
|
if (data && data.public_id !== '') {
|
||||||
onImageUpload({cloudinaryPublicId: data.public_id})
|
onImageUpload({ cloudinaryPublicId: data.public_id })
|
||||||
setCloudinaryId(data.public_id)
|
setCloudinaryId(data.public_id)
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
}
|
}
|
||||||
@@ -46,62 +46,85 @@ export default function ImageUploader({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Drag and Drop
|
// Drag and Drop
|
||||||
const onDrop = useCallback(acceptedFiles => {
|
const onDrop = useCallback((acceptedFiles) => {
|
||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
const fileReader = new FileReader()
|
const fileReader = new FileReader()
|
||||||
fileReader.onload = () => {
|
fileReader.onload = () => {
|
||||||
setFile(fileReader.result)
|
setFile(fileReader.result)
|
||||||
}
|
}
|
||||||
fileReader.readAsDataURL(acceptedFiles[0])
|
fileReader.readAsDataURL(acceptedFiles[0])
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
||||||
return (
|
return (
|
||||||
<div className={'relative overflow-hidden '+ (!imageUrl && isEditable ? 'border ' : '') + className} style={{paddingBottom: `${1/aspectRatio*100}%`}}>
|
<div
|
||||||
|
className={
|
||||||
|
'relative overflow-hidden ' +
|
||||||
|
(!imageUrl && isEditable ? 'border ' : '') +
|
||||||
|
className
|
||||||
|
}
|
||||||
|
style={{ paddingBottom: `${(1 / aspectRatio) * 100}%` }}
|
||||||
|
>
|
||||||
<div className="absolute w-full h-full" {...getRootProps()}>
|
<div className="absolute w-full h-full" {...getRootProps()}>
|
||||||
{cloudinaryId && isEditable && <button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
|
{cloudinaryId && isEditable && (
|
||||||
<Svg name="pencil" strokeWidth={2} className="text-gray-300 h-24 w-24" />
|
<button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
|
||||||
</button>}
|
<Svg
|
||||||
|
name="pencil"
|
||||||
|
strokeWidth={2}
|
||||||
|
className="text-gray-300 h-24 w-24"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{isEditable && <input {...getInputProps()} />}
|
{isEditable && <input {...getInputProps()} />}
|
||||||
{(cloudinaryId || !isEditable) && <div className="relative overflow-hidden w-full h-full">
|
{(cloudinaryId || !isEditable) && (
|
||||||
<CloudinaryImage
|
<div className="relative overflow-hidden w-full h-full">
|
||||||
className="object-cover w-full h-full shadow overflow-hidden"
|
<CloudinaryImage
|
||||||
cloudName="irevdev"
|
className="object-cover w-full h-full shadow overflow-hidden"
|
||||||
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
|
cloudName="irevdev"
|
||||||
width={width}
|
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
|
||||||
crop="scale"
|
width={width}
|
||||||
/>
|
crop="scale"
|
||||||
</div>}
|
/>
|
||||||
{!cloudinaryId && <button className="absolute inset-0"></button>}
|
|
||||||
{!cloudinaryId && isEditable && <div className="text-indigo-500 flex items-center justify-center rounded-lg w-full h-full">
|
|
||||||
<div className="px-6 text-center">
|
|
||||||
Drop files here ...
|
|
||||||
or <span className="group flex w-full items-center justify-center py-2">
|
|
||||||
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">upload</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
)}
|
||||||
|
{!cloudinaryId && <button className="absolute inset-0"></button>}
|
||||||
|
{!cloudinaryId && isEditable && (
|
||||||
|
<div className="text-indigo-500 flex items-center justify-center rounded-lg w-full h-full">
|
||||||
|
<div className="px-6 text-center">
|
||||||
|
Drop files here ... or{' '}
|
||||||
|
<span className="group flex w-full items-center justify-center py-2">
|
||||||
|
<span className="bg-indigo-500 shadow rounded text-gray-200 cursor-pointer p-2 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-150">
|
||||||
|
upload
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
<Dialog open={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||||
open={isModalOpen}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<ReactCrop src={file} crop={crop} onImageLoaded={(image) => setImageObj(image)} onChange={newCrop => setCrop(newCrop)} />
|
<ReactCrop
|
||||||
<Button onClick={handleImageUpload} variant="outlined">Upload</Button>
|
src={file}
|
||||||
|
crop={crop}
|
||||||
|
onImageLoaded={(image) => setImageObj(image)}
|
||||||
|
onChange={(newCrop) => setCrop(newCrop)}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleImageUpload} variant="outlined">
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCroppedImg(image, crop, fileName) {
|
function getCroppedImg(image, crop, fileName) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas')
|
||||||
const scaleX = image.naturalWidth / image.width;
|
const scaleX = image.naturalWidth / image.width
|
||||||
const scaleY = image.naturalHeight / image.height;
|
const scaleY = image.naturalHeight / image.height
|
||||||
canvas.width = crop.width;
|
canvas.width = crop.width
|
||||||
canvas.height = crop.height;
|
canvas.height = crop.height
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d')
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
image,
|
image,
|
||||||
crop.x * scaleX,
|
crop.x * scaleX,
|
||||||
@@ -112,16 +135,20 @@ function getCroppedImg(image, crop, fileName) {
|
|||||||
0,
|
0,
|
||||||
crop.width,
|
crop.width,
|
||||||
crop.height
|
crop.height
|
||||||
);
|
)
|
||||||
|
|
||||||
// As Base64 string
|
// As Base64 string
|
||||||
// const base64Image = canvas.toDataURL('image/jpeg');
|
// const base64Image = canvas.toDataURL('image/jpeg');
|
||||||
|
|
||||||
// As a blob
|
// As a blob
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
canvas.toBlob(blob => {
|
canvas.toBlob(
|
||||||
blob.name = fileName;
|
(blob) => {
|
||||||
resolve(blob);
|
blob.name = fileName
|
||||||
}, 'image/jpeg', 1);
|
resolve(blob)
|
||||||
});
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
1
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,39 @@ export const generated = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>AspectRatio:1, no initial image, editable</h3>
|
<h3>AspectRatio:1, no initial image, editable</h3>
|
||||||
<
|
<ImageUploader
|
||||||
ImageUploader
|
onImageUpload={({ cloudinaryPublicId }) =>
|
||||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
console.log(cloudinaryPublicId)
|
||||||
|
}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
isEditable={true}
|
isEditable={true}
|
||||||
className={"bg-red-400 rounded-half rounded-br-xl"}
|
className={'bg-red-400 rounded-half rounded-br-xl'}
|
||||||
/>
|
/>
|
||||||
<h3>AspectRatio 16:9, no initial image, editable</h3>
|
<h3>AspectRatio 16:9, no initial image, editable</h3>
|
||||||
<
|
<ImageUploader
|
||||||
ImageUploader
|
onImageUpload={({ cloudinaryPublicId }) =>
|
||||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
console.log(cloudinaryPublicId)
|
||||||
aspectRatio={16/9}
|
}
|
||||||
|
aspectRatio={16 / 9}
|
||||||
isEditable={true}
|
isEditable={true}
|
||||||
className={"bg-red-400 rounded-xl"}
|
className={'bg-red-400 rounded-xl'}
|
||||||
imageUrl="CadHub/inakek2urbreynblzhgt"
|
imageUrl="CadHub/inakek2urbreynblzhgt"
|
||||||
/>
|
/>
|
||||||
<h3>AspectRatio:1, no initial image, NOT editable</h3>
|
<h3>AspectRatio:1, no initial image, NOT editable</h3>
|
||||||
<
|
<ImageUploader
|
||||||
ImageUploader
|
onImageUpload={({ cloudinaryPublicId }) =>
|
||||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
console.log(cloudinaryPublicId)
|
||||||
|
}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
className={"rounded-half rounded-br-xl"}
|
className={'rounded-half rounded-br-xl'}
|
||||||
/>
|
/>
|
||||||
<h3>AspectRatio ,16:9 no initial image, NOT editable</h3>
|
<h3>AspectRatio ,16:9 no initial image, NOT editable</h3>
|
||||||
<
|
<ImageUploader
|
||||||
ImageUploader
|
onImageUpload={({ cloudinaryPublicId }) =>
|
||||||
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
|
console.log(cloudinaryPublicId)
|
||||||
aspectRatio={16/9}
|
}
|
||||||
className={"rounded-xl"}
|
aspectRatio={16 / 9}
|
||||||
|
className={'rounded-xl'}
|
||||||
imageUrl="CadHub/inakek2urbreynblzhgt"
|
imageUrl="CadHub/inakek2urbreynblzhgt"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { getActiveClasses } from 'get-active-classes'
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
|
||||||
const InputText = ({value, isEditable, onChange ,className}) => {
|
const InputText = ({ value, isEditable, onChange, className }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={getActiveClasses('relative inline-block', {'hidden': !isEditable}, className)}>
|
<div
|
||||||
|
className={getActiveClasses(
|
||||||
|
'relative inline-block',
|
||||||
|
{ hidden: !isEditable },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner bg-gray-100" />
|
<div className="absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner bg-gray-100" />
|
||||||
<input
|
<input
|
||||||
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
|
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
|
||||||
@@ -13,7 +19,15 @@ const InputText = ({value, isEditable, onChange ,className}) => {
|
|||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={getActiveClasses('pl-2 text-indigo-800 font-medium mb-px pb-px',{'hidden': isEditable}, className)}>{value}</span>
|
<span
|
||||||
|
className={getActiveClasses(
|
||||||
|
'pl-2 text-indigo-800 font-medium mb-px pb-px',
|
||||||
|
{ hidden: isEditable },
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { useAuth } from '@redwoodjs/auth'
|
|||||||
import PartProfile from 'src/components/PartProfile'
|
import PartProfile from 'src/components/PartProfile'
|
||||||
|
|
||||||
export const QUERY = gql`
|
export const QUERY = gql`
|
||||||
query FIND_PART_BY_USERNAME_TITLE($userName: String!, $partTitle: String, $currentUserId: String) {
|
query FIND_PART_BY_USERNAME_TITLE(
|
||||||
|
$userName: String!
|
||||||
|
$partTitle: String
|
||||||
|
$currentUserId: String
|
||||||
|
) {
|
||||||
userPart: userName(userName: $userName) {
|
userPart: userName(userName: $userName) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -42,7 +46,7 @@ export const QUERY = gql`
|
|||||||
|
|
||||||
const UPDATE_PART_MUTATION = gql`
|
const UPDATE_PART_MUTATION = gql`
|
||||||
mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) {
|
mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) {
|
||||||
updatePart:updatePart(id: $id, input: $input) {
|
updatePart: updatePart(id: $id, input: $input) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
user {
|
user {
|
||||||
@@ -66,7 +70,7 @@ const CREATE_PART_MUTATION = gql`
|
|||||||
`
|
`
|
||||||
const TOGGLE_REACTION_MUTATION = gql`
|
const TOGGLE_REACTION_MUTATION = gql`
|
||||||
mutation ToggleReactionMutation($input: TogglePartReactionInput!) {
|
mutation ToggleReactionMutation($input: TogglePartReactionInput!) {
|
||||||
togglePartReaction(input: $input){
|
togglePartReaction(input: $input) {
|
||||||
id
|
id
|
||||||
emote
|
emote
|
||||||
}
|
}
|
||||||
@@ -87,23 +91,33 @@ export const Empty = () => <div>Empty</div>
|
|||||||
|
|
||||||
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
export const Failure = ({ error }) => <div>Error: {error.message}</div>
|
||||||
|
|
||||||
export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
|
export const Success = ({ userPart, variables: { isEditable }, refetch }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const { addMessage } = useFlash()
|
const { addMessage } = useFlash()
|
||||||
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
||||||
onCompleted: ({updatePart}) => {
|
onCompleted: ({ updatePart }) => {
|
||||||
navigate(routes.part2({userName: updatePart.user.userName, partTitle: updatePart.title}))
|
navigate(
|
||||||
|
routes.part2({
|
||||||
|
userName: updatePart.user.userName,
|
||||||
|
partTitle: updatePart.title,
|
||||||
|
})
|
||||||
|
)
|
||||||
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
addMessage('Part updated.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const [createUser] = useMutation(CREATE_PART_MUTATION, {
|
const [createUser] = useMutation(CREATE_PART_MUTATION, {
|
||||||
onCompleted: ({createPart}) => {
|
onCompleted: ({ createPart }) => {
|
||||||
navigate(routes.part2({userName: createPart?.user?.userName, partTitle: createPart?.title}))
|
navigate(
|
||||||
|
routes.part2({
|
||||||
|
userName: createPart?.user?.userName,
|
||||||
|
partTitle: createPart?.title,
|
||||||
|
})
|
||||||
|
)
|
||||||
addMessage('Part Created.', { classes: 'rw-flash-success' })
|
addMessage('Part Created.', { classes: 'rw-flash-success' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const onSave = (id, input) => {
|
const onSave = (id, input) => {
|
||||||
if(!id) {
|
if (!id) {
|
||||||
createUser({ variables: { input } })
|
createUser({ variables: { input } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -111,30 +125,42 @@ export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
|
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
|
||||||
onCompleted: () => refetch()
|
onCompleted: () => refetch(),
|
||||||
})
|
})
|
||||||
const onReaction = (emote) => toggleReaction({variables: {input: {
|
const onReaction = (emote) =>
|
||||||
emote,
|
toggleReaction({
|
||||||
userId: currentUser.sub,
|
variables: {
|
||||||
partId: userPart?.Part?.id,
|
input: {
|
||||||
}}})
|
emote,
|
||||||
|
userId: currentUser.sub,
|
||||||
|
partId: userPart?.Part?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const [createComment] = useMutation(CREATE_COMMENT_MUTATION, {
|
const [createComment] = useMutation(CREATE_COMMENT_MUTATION, {
|
||||||
onCompleted: () => refetch()
|
onCompleted: () => refetch(),
|
||||||
})
|
})
|
||||||
const onComment = (text) => createComment({variables: {input: {
|
const onComment = (text) =>
|
||||||
text,
|
createComment({
|
||||||
userId: currentUser.sub,
|
variables: {
|
||||||
partId: userPart?.Part?.id,
|
input: {
|
||||||
}}})
|
text,
|
||||||
|
userId: currentUser.sub,
|
||||||
|
partId: userPart?.Part?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return <PartProfile
|
return (
|
||||||
userPart={userPart}
|
<PartProfile
|
||||||
onSave={onSave}
|
userPart={userPart}
|
||||||
loading={loading}
|
onSave={onSave}
|
||||||
error={error}
|
loading={loading}
|
||||||
isEditable={isEditable}
|
error={error}
|
||||||
onReaction={onReaction}
|
isEditable={isEditable}
|
||||||
onComment={onComment}
|
onReaction={onReaction}
|
||||||
/>
|
onComment={onComment}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,69 @@
|
|||||||
import {useState, useEffect} from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
import Editor from "rich-markdown-editor";
|
import Editor from 'rich-markdown-editor'
|
||||||
|
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import Breadcrumb from 'src/components/Breadcrumb'
|
import Breadcrumb from 'src/components/Breadcrumb'
|
||||||
import EmojiReaction from 'src/components/EmojiReaction'
|
import EmojiReaction from 'src/components/EmojiReaction'
|
||||||
import Button from 'src/components/Button'
|
import Button from 'src/components/Button'
|
||||||
import { countEmotes } from 'src/helpers/emote'
|
import { countEmotes } from 'src/helpers/emote'
|
||||||
import { getActiveClasses } from 'get-active-classes';
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
|
||||||
const PartProfile = ({
|
const PartProfile = ({
|
||||||
userPart,
|
userPart,
|
||||||
isEditable,
|
isEditable,
|
||||||
onSave,
|
onSave,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
onReaction,
|
onReaction,
|
||||||
onComment,
|
onComment,
|
||||||
}) => {
|
}) => {
|
||||||
const [comment, setComment] = useState('')
|
const [comment, setComment] = useState('')
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const canEdit = currentUser?.sub === userPart.id
|
const canEdit = currentUser?.sub === userPart.id
|
||||||
const part = userPart?.Part
|
const part = userPart?.Part
|
||||||
const emotes = countEmotes(part?.Reaction)
|
const emotes = countEmotes(part?.Reaction)
|
||||||
const userEmotes = part?.userReactions.map(({emote}) => emote)
|
const userEmotes = part?.userReactions.map(({ emote }) => emote)
|
||||||
useEffect(() => {isEditable &&
|
useEffect(() => {
|
||||||
!canEdit &&
|
isEditable &&
|
||||||
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))},
|
!canEdit &&
|
||||||
[currentUser])
|
navigate(
|
||||||
|
routes.part2({ userName: userPart.userName, partTitle: part.title })
|
||||||
|
)
|
||||||
|
}, [currentUser])
|
||||||
const [input, setInput] = useState({
|
const [input, setInput] = useState({
|
||||||
title: part?.title,
|
title: part?.title,
|
||||||
mainImage: part?.mainImage,
|
mainImage: part?.mainImage,
|
||||||
description: part?.description,
|
description: part?.description,
|
||||||
userId: userPart?.id,
|
userId: userPart?.id,
|
||||||
})
|
})
|
||||||
const setProperty = (property, value) => setInput({
|
const setProperty = (property, value) =>
|
||||||
...input,
|
setInput({
|
||||||
[property]: value,
|
...input,
|
||||||
})
|
[property]: value,
|
||||||
const onTitleChange = ({target}) => setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
|
})
|
||||||
const onDescriptionChange = (description) => setProperty('description', description())
|
const onTitleChange = ({ target }) =>
|
||||||
const onImageUpload = ({cloudinaryPublicId}) => setProperty('mainImage', cloudinaryPublicId)
|
setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
|
||||||
|
const onDescriptionChange = (description) =>
|
||||||
|
setProperty('description', description())
|
||||||
|
const onImageUpload = ({ cloudinaryPublicId }) =>
|
||||||
|
setProperty('mainImage', cloudinaryPublicId)
|
||||||
const onEditSaveClick = () => {
|
const onEditSaveClick = () => {
|
||||||
if (isEditable) {
|
if (isEditable) {
|
||||||
input.title && onSave(part?.id, input)
|
input.title && onSave(part?.id, input)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate(routes.editPart2({userName: userPart.userName, partTitle: part.title}))
|
navigate(
|
||||||
|
routes.editPart2({ userName: userPart.userName, partTitle: part.title })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid mt-20 gap-8" style={{gridTemplateColumns: 'auto 12rem minmax(12rem, 42rem) auto'}}>
|
<div
|
||||||
|
className="grid mt-20 gap-8"
|
||||||
|
style={{ gridTemplateColumns: 'auto 12rem minmax(12rem, 42rem) auto' }}
|
||||||
|
>
|
||||||
{/* Side column */}
|
{/* Side column */}
|
||||||
<aside className="col-start-2 relative">
|
<aside className="col-start-2 relative">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
@@ -62,7 +73,11 @@ const PartProfile = ({
|
|||||||
imageUrl={userPart.image}
|
imageUrl={userPart.image}
|
||||||
width={300}
|
width={300}
|
||||||
/>
|
/>
|
||||||
<h4 className="text-indigo-800 text-xl underline text-right py-4"><Link to={routes.user2({userName: userPart.userName})}>{userPart?.name}</Link></h4>
|
<h4 className="text-indigo-800 text-xl underline text-right py-4">
|
||||||
|
<Link to={routes.user2({ userName: userPart.userName })}>
|
||||||
|
{userPart?.name}
|
||||||
|
</Link>
|
||||||
|
</h4>
|
||||||
<div className="h-px bg-indigo-200 mb-4" />
|
<div className="h-px bg-indigo-200 mb-4" />
|
||||||
<EmojiReaction
|
<EmojiReaction
|
||||||
emotes={emotes}
|
emotes={emotes}
|
||||||
@@ -85,29 +100,43 @@ const PartProfile = ({
|
|||||||
>
|
>
|
||||||
Open IDE
|
Open IDE
|
||||||
</Button>
|
</Button>
|
||||||
{canEdit && <Button
|
{canEdit && (
|
||||||
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
|
<Button
|
||||||
shouldAnimateHover
|
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
|
||||||
iconName={isEditable ? 'save' : 'pencil'}
|
shouldAnimateHover
|
||||||
onClick={onEditSaveClick}
|
iconName={isEditable ? 'save' : 'pencil'}
|
||||||
>
|
onClick={onEditSaveClick}
|
||||||
{isEditable ? 'Save Details' : 'Edit Details'}
|
>
|
||||||
</Button>}
|
{isEditable ? 'Save Details' : 'Edit Details'}
|
||||||
{isEditable && <div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />}
|
</Button>
|
||||||
|
)}
|
||||||
|
{isEditable && (
|
||||||
|
<div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* main project center column */}
|
{/* main project center column */}
|
||||||
<section className="col-start-3">
|
<section className="col-start-3">
|
||||||
<Breadcrumb className="inline" onPartTitleChange={isEditable && onTitleChange} userName={userPart.userName} partTitle={input?.title}/>
|
<Breadcrumb
|
||||||
{ !!(input?.mainImage || isEditable) && <ImageUploader
|
className="inline"
|
||||||
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
|
onPartTitleChange={isEditable && onTitleChange}
|
||||||
onImageUpload={onImageUpload}
|
userName={userPart.userName}
|
||||||
aspectRatio={16/9}
|
partTitle={input?.title}
|
||||||
isEditable={isEditable}
|
/>
|
||||||
imageUrl={input?.mainImage}
|
{!!(input?.mainImage || isEditable) && (
|
||||||
width={1010}
|
<ImageUploader
|
||||||
/>}
|
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
|
||||||
<div name="description" className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-8 min-h-md">
|
onImageUpload={onImageUpload}
|
||||||
|
aspectRatio={16 / 9}
|
||||||
|
isEditable={isEditable}
|
||||||
|
imageUrl={input?.mainImage}
|
||||||
|
width={1010}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
name="description"
|
||||||
|
className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-8 min-h-md"
|
||||||
|
>
|
||||||
<Editor
|
<Editor
|
||||||
defaultValue={part?.description || ''}
|
defaultValue={part?.description || ''}
|
||||||
readOnly={!isEditable}
|
readOnly={!isEditable}
|
||||||
@@ -115,56 +144,66 @@ const PartProfile = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* comments */}
|
{/* comments */}
|
||||||
{ !isEditable && <>
|
{!isEditable && (
|
||||||
<div className="h-px bg-indigo-200 mt-8" />
|
<>
|
||||||
<h3 className="text-indigo-800 text-lg font-roboto tracking-wider mb-4" >Comments</h3>
|
<div className="h-px bg-indigo-200 mt-8" />
|
||||||
|
<h3 className="text-indigo-800 text-lg font-roboto tracking-wider mb-4">
|
||||||
|
Comments
|
||||||
|
</h3>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{part?.Comment.map(({text, user, id}) => (
|
{part?.Comment.map(({ text, user, id }) => (
|
||||||
<li key={id} className="flex mb-6">
|
<li key={id} className="flex mb-6">
|
||||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
|
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
className=""
|
className=""
|
||||||
onImageUpload={() => {}}
|
onImageUpload={() => {}}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
imageUrl={user?.image}
|
imageUrl={user?.image}
|
||||||
width={50}
|
width={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 font-roboto">
|
||||||
|
<div className="text-gray-800 font-bold text-lg mb-1">
|
||||||
|
<Link to={routes.user2({ userName: user.userName })}>
|
||||||
|
{user.userName}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700 p-3 rounded bg-gray-200 shadow">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{currentUser && (
|
||||||
|
<>
|
||||||
|
<div className="mt-12 ml-12">
|
||||||
|
<textarea
|
||||||
|
className="w-full h-32 rounded-lg shadow-inner outline-none resize-none p-3"
|
||||||
|
placeholder="Leave a comment"
|
||||||
|
value={comment}
|
||||||
|
onChange={({ target }) => setComment(target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 font-roboto">
|
<Button
|
||||||
<div className="text-gray-800 font-bold text-lg mb-1"><Link to={routes.user2({userName: user.userName})}>
|
className={getActiveClasses(
|
||||||
{user.userName}
|
'ml-auto hover:shadow-lg bg-gray-300 mt-4 mb-20',
|
||||||
</Link></div>
|
{ 'bg-indigo-200': currentUser }
|
||||||
<div className="text-gray-700 p-3 rounded bg-gray-200 shadow">
|
)}
|
||||||
{text}
|
shouldAnimateHover
|
||||||
</div>
|
disabled={!currentUser}
|
||||||
</div>
|
iconName={'save'}
|
||||||
</li>
|
onClick={() => onComment(comment)}
|
||||||
))}
|
>
|
||||||
</ul>
|
Comment
|
||||||
{currentUser && <>
|
</Button>
|
||||||
<div className="mt-12 ml-12">
|
</>
|
||||||
<textarea
|
)}
|
||||||
className="w-full h-32 rounded-lg shadow-inner outline-none resize-none p-3"
|
</>
|
||||||
placeholder="Leave a comment"
|
)}
|
||||||
value={comment}
|
|
||||||
onChange={({target}) => setComment(target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className={getActiveClasses("ml-auto hover:shadow-lg bg-gray-300 mt-4 mb-20", {'bg-indigo-200': currentUser})}
|
|
||||||
shouldAnimateHover
|
|
||||||
disabled={!currentUser}
|
|
||||||
iconName={'save'}
|
|
||||||
onClick={() => onComment(comment)}
|
|
||||||
>Comment</Button>
|
|
||||||
</>}
|
|
||||||
</>}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import { countEmotes } from 'src/helpers/emote'
|
|||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
|
|
||||||
const PartsList = ({ parts }) => {
|
const PartsList = ({ parts }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-6xl mx-auto mt-20">
|
<section className="max-w-6xl mx-auto mt-20">
|
||||||
<ul className="grid gap-x-8 gap-y-12 items-center mx-4 relative" style={{gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))'}}>
|
<ul
|
||||||
{parts.map(({title, mainImage, user, Reaction}) => (
|
className="grid gap-x-8 gap-y-12 items-center mx-4 relative"
|
||||||
<li className="rounded-lg shadow-md hover:shadow-lg mx-px transform hover:-translate-y-px transition-all duration-150" key={`${user?.userName}--${title}`}>
|
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}
|
||||||
|
>
|
||||||
|
{parts.map(({ title, mainImage, user, Reaction }) => (
|
||||||
|
<li
|
||||||
|
className="rounded-lg shadow-md hover:shadow-lg mx-px transform hover:-translate-y-px transition-all duration-150"
|
||||||
|
key={`${user?.userName}--${title}`}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
to={routes.part2({userName: user?.userName, partTitle: title})}
|
to={routes.part2({ userName: user?.userName, partTitle: title })}
|
||||||
>
|
>
|
||||||
<div className="flex items-center p-2 bg-gray-200 border-gray-300 rounded-t-lg border-t border-l border-r">
|
<div className="flex items-center p-2 bg-gray-200 border-gray-300 rounded-t-lg border-t border-l border-r">
|
||||||
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow">
|
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow">
|
||||||
@@ -23,7 +28,9 @@ const PartsList = ({ parts }) => {
|
|||||||
width={50}
|
width={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-ropa-sans ml-3 text-lg text-indigo-900">{title}</span>
|
<span className="font-ropa-sans ml-3 text-lg text-indigo-900">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-hidden relative rounded-b-lg">
|
<div className="w-full overflow-hidden relative rounded-b-lg">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
@@ -33,18 +40,27 @@ const PartsList = ({ parts }) => {
|
|||||||
imageUrl={mainImage}
|
imageUrl={mainImage}
|
||||||
width={700}
|
width={700}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0" style={{background: 'linear-gradient(19.04deg, rgba(62, 44, 118, 0.46) 10.52%, rgba(60, 54, 107, 0) 40.02%)'}} />
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(19.04deg, rgba(62, 44, 118, 0.46) 10.52%, rgba(60, 54, 107, 0) 40.02%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-x-0 bottom-0 -mb-4 mr-4 flex justify-end">{countEmotes(Reaction).map(({emoji, count}) => (
|
<div className="absolute inset-x-0 bottom-0 -mb-4 mr-4 flex justify-end">
|
||||||
<div key={emoji} className="h-8 w-8 overflow-hidden ml-2 p-1 rounded-full bg-opacity-75 bg-gray-200 border border-gray-300 shadow-md flex items-center justify-between">
|
{countEmotes(Reaction).map(({ emoji, count }) => (
|
||||||
<div className="-ml-px text-sm w-1">
|
<div
|
||||||
{emoji}
|
key={emoji}
|
||||||
|
className="h-8 w-8 overflow-hidden ml-2 p-1 rounded-full bg-opacity-75 bg-gray-200 border border-gray-300 shadow-md flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="-ml-px text-sm w-1">{emoji}</div>
|
||||||
|
<div className="text-sm pl-1 font-ropa-sans text-gray-800">
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm pl-1 font-ropa-sans text-gray-800">
|
))}
|
||||||
{count}
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import {Fragment} from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import InputText from 'src/components/InputText'
|
import InputText from 'src/components/InputText'
|
||||||
|
|
||||||
const ProfileTextInput = ({fields, isEditable, onChange= () => {}}) => {
|
const ProfileTextInput = ({ fields, isEditable, onChange = () => {} }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="grid items-center" style={{gridTemplateColumns: 'auto 1fr'}}>
|
<div
|
||||||
{Object.entries(fields).map(([property, value]) => (<Fragment key={property}>
|
className="grid items-center"
|
||||||
<span className="capitalize text-gray-500 text-sm align-middle my-3">{property}:</span>
|
style={{ gridTemplateColumns: 'auto 1fr' }}
|
||||||
|
>
|
||||||
|
{Object.entries(fields).map(([property, value]) => (
|
||||||
|
<Fragment key={property}>
|
||||||
|
<span className="capitalize text-gray-500 text-sm align-middle my-3">
|
||||||
|
{property}:
|
||||||
|
</span>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
className="text-xl"
|
className="text-xl"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={({target}) => onChange({...fields, [property]: target.value})}
|
onChange={({ target }) =>
|
||||||
isEditable={isEditable}
|
onChange({ ...fields, [property]: target.value })
|
||||||
/>
|
}
|
||||||
|
isEditable={isEditable}
|
||||||
</Fragment>))}
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +1,113 @@
|
|||||||
const Svg = ({name, className: className2, strokeWidth = 2}) => {
|
const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
|
||||||
|
|
||||||
const svgs = {
|
const svgs = {
|
||||||
"chevron-down": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
'chevron-down': (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M19 9l-7 7-7-7" />
|
<svg
|
||||||
</svg>,
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
"dots-vertical": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
fill="none"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
viewBox="0 0 24 24"
|
||||||
</svg>,
|
stroke="currentColor"
|
||||||
"pencil": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
<path
|
||||||
</svg>,
|
strokeLinecap="round"
|
||||||
"plus":<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
strokeLinejoin="round"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
strokeWidth={strokeWidth}
|
||||||
</svg>,
|
d="M19 9l-7 7-7-7"
|
||||||
"plus-circle": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
/>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
</svg>
|
||||||
</svg>,
|
),
|
||||||
"save": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
'dots-vertical': (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
<svg
|
||||||
</svg>,
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
"terminal": <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
fill="none"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={strokeWidth} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
viewBox="0 0 24 24"
|
||||||
</svg>,
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
pencil: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
plus: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
'plus-circle': (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
save: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
terminal: (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={className2 || "h-10 w-10"}>
|
return <div className={className2 || 'h-10 w-10'}>{svgs[name]}</div>
|
||||||
{svgs[name]}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Svg
|
export default Svg
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ export const Loading = () => <div>Loading...</div>
|
|||||||
|
|
||||||
export const Empty = () => <div>User not found</div>
|
export const Empty = () => <div>User not found</div>
|
||||||
|
|
||||||
export const Success = ({user}) => {
|
export const Success = ({ user }) => {
|
||||||
return <UserProfile user={user} />
|
return <UserProfile user={user} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
import {useState, useEffect} from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { navigate, routes } from '@redwoodjs/router'
|
import { navigate, routes } from '@redwoodjs/router'
|
||||||
import Editor from "rich-markdown-editor";
|
import Editor from 'rich-markdown-editor'
|
||||||
|
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import Button from 'src/components/Button'
|
import Button from 'src/components/Button'
|
||||||
import ProfileTextInput from 'src/components/ProfileTextInput'
|
import ProfileTextInput from 'src/components/ProfileTextInput'
|
||||||
|
|
||||||
|
const UserProfile = ({ user, isEditable, loading, onSave, error }) => {
|
||||||
const UserProfile = ({user, isEditable, loading, onSave, error}) => {
|
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const canEdit = currentUser?.sub === user.id
|
const canEdit = currentUser?.sub === user.id
|
||||||
useEffect(() => {isEditable &&
|
useEffect(() => {
|
||||||
!canEdit &&
|
isEditable &&
|
||||||
navigate(routes.user2({userName: user.userName}))},
|
!canEdit &&
|
||||||
[currentUser])
|
navigate(routes.user2({ userName: user.userName }))
|
||||||
|
}, [currentUser])
|
||||||
const [input, setInput] = useState({
|
const [input, setInput] = useState({
|
||||||
userName: user.userName,
|
userName: user.userName,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
bio: user.bio,
|
bio: user.bio,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
})
|
})
|
||||||
const {userName, name} = input
|
const { userName, name } = input
|
||||||
const editableTextFields = {userName, name}
|
const editableTextFields = { userName, name }
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="max-w-2xl mx-auto mt-20 ">
|
<section className="max-w-2xl mx-auto mt-20 ">
|
||||||
<div className="flex" >
|
<div className="flex">
|
||||||
<div className="w-40 flex-shrink-0">
|
<div className="w-40 flex-shrink-0">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
|
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
|
||||||
onImageUpload={({cloudinaryPublicId: image}) => setInput({
|
onImageUpload={({ cloudinaryPublicId: image }) =>
|
||||||
...input,
|
setInput({
|
||||||
image,
|
...input,
|
||||||
})}
|
image,
|
||||||
|
})
|
||||||
|
}
|
||||||
aspectRatio={1}
|
aspectRatio={1}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
imageUrl={user.image}
|
imageUrl={user.image}
|
||||||
@@ -41,29 +43,53 @@ const UserProfile = ({user, isEditable, loading, onSave, error}) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-6 flex flex-col justify-between">
|
<div className="ml-6 flex flex-col justify-between">
|
||||||
<ProfileTextInput fields={editableTextFields} onChange={({userName, name}) => setInput({
|
<ProfileTextInput
|
||||||
...input,
|
fields={editableTextFields}
|
||||||
name,
|
onChange={({ userName, name }) =>
|
||||||
userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'),
|
setInput({
|
||||||
})} isEditable={isEditable}/>
|
...input,
|
||||||
{isEditable ?
|
name,
|
||||||
<Button className="bg-indigo-200" iconName="plus" onClick={() => onSave(user.userName, input)}>Save Profile</Button> : // TODO replace pencil with a save icon
|
userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'),
|
||||||
canEdit ?
|
})
|
||||||
<Button className="bg-indigo-200" iconName="pencil" onClick={() => navigate(routes.editUser2({userName: user.userName}))}>Edit Profile</Button>:
|
}
|
||||||
null
|
isEditable={isEditable}
|
||||||
}
|
/>
|
||||||
|
{isEditable ? (
|
||||||
|
<Button
|
||||||
|
className="bg-indigo-200"
|
||||||
|
iconName="plus"
|
||||||
|
onClick={() => onSave(user.userName, input)}
|
||||||
|
>
|
||||||
|
Save Profile
|
||||||
|
</Button> // TODO replace pencil with a save icon
|
||||||
|
) : canEdit ? (
|
||||||
|
<Button
|
||||||
|
className="bg-indigo-200"
|
||||||
|
iconName="pencil"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(routes.editUser2({ userName: user.userName }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h3 className="text-3xl text-gray-500 font-ropa-sans">Bio:</h3>
|
<h3 className="text-3xl text-gray-500 font-ropa-sans">Bio:</h3>
|
||||||
<div name="description" className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-6 min-h-md">
|
<div
|
||||||
|
name="description"
|
||||||
|
className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-6 min-h-md"
|
||||||
|
>
|
||||||
<Editor
|
<Editor
|
||||||
defaultValue={user.bio || ''}
|
defaultValue={user.bio || ''}
|
||||||
readOnly={!isEditable}
|
readOnly={!isEditable}
|
||||||
onChange={(bioFn) => setInput({
|
onChange={(bioFn) =>
|
||||||
...input,
|
setInput({
|
||||||
bio: bioFn(),
|
...input,
|
||||||
})}
|
bio: bioFn(),
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ export const countEmotes = (reactions = []) => {
|
|||||||
// would be good to do this sever side
|
// would be good to do this sever side
|
||||||
// counting unique emojis, and limiting to the 5 largest
|
// counting unique emojis, and limiting to the 5 largest
|
||||||
const emoteCounts = {}
|
const emoteCounts = {}
|
||||||
reactions.forEach(({emote}) => {
|
reactions.forEach(({ emote }) => {
|
||||||
emoteCounts[emote] = emoteCounts[emote] ? emoteCounts[emote] + 1 : 1
|
emoteCounts[emote] = emoteCounts[emote] ? emoteCounts[emote] + 1 : 1
|
||||||
})
|
})
|
||||||
// TODO the sort is causing the emotes to jump around after the user clicks one, not ideal
|
// TODO the sort is causing the emotes to jump around after the user clicks one, not ideal
|
||||||
return Object.entries(emoteCounts).map(([emoji, count]) => ({emoji, count})).sort((a,b) => a.count-b.count).slice(-5)
|
return Object.entries(emoteCounts)
|
||||||
|
.map(([emoji, count]) => ({ emoji, count }))
|
||||||
|
.sort((a, b) => a.count - b.count)
|
||||||
|
.slice(-5)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link, navigate ,routes } from '@redwoodjs/router'
|
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
import { useAuth } from '@redwoodjs/auth'
|
||||||
import { Flash } from '@redwoodjs/web'
|
import { Flash } from '@redwoodjs/web'
|
||||||
import Tooltip from '@material-ui/core/Tooltip';
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
import { useQuery } from '@redwoodjs/web'
|
import { useQuery } from '@redwoodjs/web'
|
||||||
import Popover from '@material-ui/core/Popover'
|
import Popover from '@material-ui/core/Popover'
|
||||||
import {getActiveClasses} from 'get-active-classes'
|
import { getActiveClasses } from 'get-active-classes'
|
||||||
|
|
||||||
export const QUERY = gql`
|
export const QUERY = gql`
|
||||||
query FIND_USER_BY_ID($id: String!) {
|
query FIND_USER_BY_ID($id: String!) {
|
||||||
@@ -17,14 +17,15 @@ export const QUERY = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
import avatar from 'src/assets/harold.jpg'
|
|
||||||
import Svg from 'src/components/Svg'
|
import Svg from 'src/components/Svg'
|
||||||
import ImageUploader from 'src/components/ImageUploader'
|
import ImageUploader from 'src/components/ImageUploader'
|
||||||
import logo from 'src/layouts/MainLayout/Logo_2.jpg'
|
import logo from 'src/layouts/MainLayout/Logo_2.jpg'
|
||||||
|
|
||||||
const MainLayout = ({ children}) => {
|
const MainLayout = ({ children }) => {
|
||||||
const { logIn, logOut, isAuthenticated, currentUser } = useAuth()
|
const { logIn, logOut, isAuthenticated, currentUser } = useAuth()
|
||||||
const {data, loading} = useQuery(QUERY, {variables: {id: currentUser?.sub}})
|
const { data, loading } = useQuery(QUERY, {
|
||||||
|
variables: { id: currentUser?.sub },
|
||||||
|
})
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [popoverId, setPopoverId] = useState(undefined)
|
const [popoverId, setPopoverId] = useState(undefined)
|
||||||
@@ -55,38 +56,59 @@ const MainLayout = ({ children}) => {
|
|||||||
<li>
|
<li>
|
||||||
<Link to={routes.home()}>
|
<Link to={routes.home()}>
|
||||||
<div className="rounded-full overflow-hidden ml-12">
|
<div className="rounded-full overflow-hidden ml-12">
|
||||||
<img src={logo}/>
|
<img src={logo} />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Tooltip title="Very alpha, there's lots of work todo" >
|
<Tooltip title="Very alpha, there's lots of work todo">
|
||||||
<div className="ml-12 flex">
|
<div className="ml-12 flex">
|
||||||
{/* Because of how specific these styles are to this heading/logo and it doesn't need to be replicated else where as well as it's very precise with the placement of "pre-alpha" I think it's appropriate. */}
|
{/* Because of how specific these styles are to this heading/logo and it doesn't need to be replicated else where as well as it's very precise with the placement of "pre-alpha" I think it's appropriate. */}
|
||||||
<h2 className="text-indigo-300 text-5xl font-ropa-sans py-1 tracking-wider" style={{letterSpacing: '0.3em'}}>CadHub</h2>
|
<h2
|
||||||
<div className="text-pink-400 text-sm font-bold font-ropa-sans" style={{paddingBottom: '2rem', marginLeft: '-1.8rem'}}>pre-alpha</div>
|
className="text-indigo-300 text-5xl font-ropa-sans py-1 tracking-wider"
|
||||||
|
style={{ letterSpacing: '0.3em' }}
|
||||||
|
>
|
||||||
|
CadHub
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
className="text-pink-400 text-sm font-bold font-ropa-sans"
|
||||||
|
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
|
||||||
|
>
|
||||||
|
pre-alpha
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="flex items-center">
|
<ul className="flex items-center">
|
||||||
<li className={getActiveClasses("mr-8 h-10 w-10 rounded-full border-2 border-gray-700 flex items-center justify-center", {'border-indigo-300': currentUser})}>
|
<li
|
||||||
{isAuthenticated && data?.user?.userName ?
|
className={getActiveClasses(
|
||||||
<Link className="h-full w-full" to={routes.newPart2({userName: data?.user?.userName})}>
|
'mr-8 h-10 w-10 rounded-full border-2 border-gray-700 flex items-center justify-center',
|
||||||
|
{ 'border-indigo-300': currentUser }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isAuthenticated && data?.user?.userName ? (
|
||||||
|
<Link
|
||||||
|
className="h-full w-full"
|
||||||
|
to={routes.newPart2({ userName: data?.user?.userName })}
|
||||||
|
>
|
||||||
<Svg name="plus" className="text-indigo-300 w-full h-full" />
|
<Svg name="plus" className="text-indigo-300 w-full h-full" />
|
||||||
</Link>:
|
</Link>
|
||||||
|
) : (
|
||||||
<Svg name="plus" className="text-gray-700 w-full h-full" />
|
<Svg name="plus" className="text-gray-700 w-full h-full" />
|
||||||
}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li className="h-10 w-10 border-1 rounded-full border-indigo-300 text-indigo-200">
|
<li className="h-10 w-10 border-1 rounded-full border-indigo-300 text-indigo-200">
|
||||||
<div aria-describedby={popoverId} onMouseOver={togglePopover}>
|
<div aria-describedby={popoverId} onMouseOver={togglePopover}>
|
||||||
{!loading && <ImageUploader
|
{!loading && (
|
||||||
className="rounded-full object-cover"
|
<ImageUploader
|
||||||
onImageUpload={() => {}}
|
className="rounded-full object-cover"
|
||||||
aspectRatio={1}
|
onImageUpload={() => {}}
|
||||||
imageUrl={data?.user?.image}
|
aspectRatio={1}
|
||||||
width={80}
|
imageUrl={data?.user?.image}
|
||||||
/>}
|
width={80}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -104,23 +126,33 @@ const MainLayout = ({ children}) => {
|
|||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{isAuthenticated && currentUser ? (
|
||||||
isAuthenticated && currentUser?
|
<div style={{ padding: '1em', width: '15em' }}>
|
||||||
<div style={{padding: '1em', width: '15em'}} >
|
<Link to={routes.user2({ userName: data?.user?.userName })}>
|
||||||
<Link to={routes.user2({userName: data?.user?.userName})}>
|
<h3 className="text-indigo-800" style={{ fontWeight: '500' }}>
|
||||||
<h3 className="text-indigo-800" style={{fontWeight: '500'}} >Hello {data?.user?.name}</h3>
|
Hello {data?.user?.name}
|
||||||
</Link>
|
</h3>
|
||||||
<hr/>
|
</Link>
|
||||||
<br/>
|
<hr />
|
||||||
<Link to={routes.editUser2({userName: data?.user?.userName})}>
|
<br />
|
||||||
<div className="text-indigo-800" >Edit Profile</div>
|
<Link to={routes.editUser2({ userName: data?.user?.userName })}>
|
||||||
</Link>
|
<div className="text-indigo-800">Edit Profile</div>
|
||||||
<a href="#" className="text-indigo-800" onClick={logOut}>Logout</a>
|
</Link>
|
||||||
</div>:
|
<a href="#" className="text-indigo-800" onClick={logOut}>
|
||||||
<div style={{padding: '1em', width: '15em'}} >
|
Logout
|
||||||
<a href="#" className="text-indigo-800 text-indigo-800" onClick={logIn}>LOGIN/SIGNUP</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
) : (
|
||||||
|
<div style={{ padding: '1em', width: '15em' }}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="text-indigo-800 text-indigo-800"
|
||||||
|
onClick={logIn}
|
||||||
|
>
|
||||||
|
LOGIN/SIGNUP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAuth } from '@redwoodjs/auth'
|
|||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import Part2Cell from 'src/components/Part2Cell'
|
import Part2Cell from 'src/components/Part2Cell'
|
||||||
|
|
||||||
const EditPart2Page = ({userName, partTitle}) => {
|
const EditPart2Page = ({ userName, partTitle }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import EditUser2Cell from 'src/components/EditUser2Cell'
|
|||||||
const UserPage = ({ userName }) => {
|
const UserPage = ({ userName }) => {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<EditUser2Cell userName={userName}/>
|
<EditUser2Cell userName={userName} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import MainLayout from 'src/layouts/MainLayout'
|
|||||||
import { initialize } from 'src/cascade/js/MainPage/CascadeMain'
|
import { initialize } from 'src/cascade/js/MainPage/CascadeMain'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
const starterCode =
|
const starterCode = `// Welcome to Cascade Studio! Here are some useful functions:
|
||||||
`// Welcome to Cascade Studio! Here are some useful functions:
|
|
||||||
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
|
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
|
||||||
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
|
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
|
||||||
// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),
|
// Offset(), Extrude(), RotatedExtrude(), Revolve(), Pipe(), Loft(),
|
||||||
@@ -25,7 +24,7 @@ Translate([0, 0, 50], Difference(sphere, [cylinderX, cylinderY, cylinderZ]));
|
|||||||
Translate([-100, 0, 100], Text3D("cadhub.xyz"));
|
Translate([-100, 0, 100], Text3D("cadhub.xyz"));
|
||||||
|
|
||||||
// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!
|
// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!
|
||||||
`;
|
`
|
||||||
|
|
||||||
const HomePage1 = () => {
|
const HomePage1 = () => {
|
||||||
const [code, setCode] = useState(starterCode)
|
const [code, setCode] = useState(starterCode)
|
||||||
@@ -34,39 +33,83 @@ const HomePage1 = () => {
|
|||||||
new initialize(sickCallback, starterCode)
|
new initialize(sickCallback, starterCode)
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div>current code {code}</div>
|
<div>current code {code}</div>
|
||||||
<BlogPostsCell/>
|
<BlogPostsCell />
|
||||||
<div>
|
<div>
|
||||||
<div id="topnav" className="topnav">
|
<div id="topnav" className="topnav">
|
||||||
<a href="https://github.com/zalo/CascadeStudio">Cascade Studio 0.0.6</a>
|
<a href="https://github.com/zalo/CascadeStudio">
|
||||||
<a href="#" id="main-proj-button" title="Sets this project to save in local storage." onClick={() => makeMainProject()}>Make Main Project</a>
|
Cascade Studio 0.0.6
|
||||||
<a href="#" title="Save Project to .json" onClick={() => saveProject()}>Save Project</a>
|
</a>
|
||||||
<label htmlFor="project-file" title="Load Project from .json">Load Project
|
<a
|
||||||
<input
|
href="#"
|
||||||
id="project-file"
|
id="main-proj-button"
|
||||||
name="project-file"
|
title="Sets this project to save in local storage."
|
||||||
type="file"
|
onClick={() => makeMainProject()}
|
||||||
accept=".json"
|
>
|
||||||
style={{display:'none'}}
|
Make Main Project
|
||||||
onInput={() => loadProject()}
|
</a>
|
||||||
/>
|
<a
|
||||||
</label>
|
href="#"
|
||||||
<a href="#" onClick={() => threejsViewport.saveShapeSTEP()}>Save STEP</a>
|
title="Save Project to .json"
|
||||||
<a href="#" onClick={() => threejsViewport.saveShapeSTL()}>Save STL</a>
|
onClick={() => saveProject()}
|
||||||
<a href="#" onClick={() => threejsViewport.saveShapeOBJ()}>Save OBJ</a>
|
>
|
||||||
<label htmlFor="files" title="Import STEP, IGES, or (ASCII) STL from File">Import STEP/IGES/STL
|
Save Project
|
||||||
<input id="files" name="files" type="file" accept=".iges,.step,.igs,.stp,.stl" multiple style={{display: 'none'}} onInput={ () =>loadFiles()}/>
|
</a>
|
||||||
</label>
|
<label htmlFor="project-file" title="Load Project from .json">
|
||||||
<a href="#" title="Clears the external step/iges/stl files stored in the project." onClick={() => clearExternalFiles()}>Clear Imported Files</a>
|
Load Project
|
||||||
<a href="" title="Resets the project and localstorage." onClick={() => {
|
<input
|
||||||
window.localStorage.clear();
|
id="project-file"
|
||||||
window.history.replaceState({}, 'Cascade Studio','?')
|
name="project-file"
|
||||||
}}>Reset Project</a>
|
type="file"
|
||||||
</div>
|
accept=".json"
|
||||||
<div id="cascade-container" style={{height:'auto'}}>
|
style={{ display: 'none' }}
|
||||||
|
onInput={() => loadProject()}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<a href="#" onClick={() => threejsViewport.saveShapeSTEP()}>
|
||||||
|
Save STEP
|
||||||
|
</a>
|
||||||
|
<a href="#" onClick={() => threejsViewport.saveShapeSTL()}>
|
||||||
|
Save STL
|
||||||
|
</a>
|
||||||
|
<a href="#" onClick={() => threejsViewport.saveShapeOBJ()}>
|
||||||
|
Save OBJ
|
||||||
|
</a>
|
||||||
|
<label
|
||||||
|
htmlFor="files"
|
||||||
|
title="Import STEP, IGES, or (ASCII) STL from File"
|
||||||
|
>
|
||||||
|
Import STEP/IGES/STL
|
||||||
|
<input
|
||||||
|
id="files"
|
||||||
|
name="files"
|
||||||
|
type="file"
|
||||||
|
accept=".iges,.step,.igs,.stp,.stl"
|
||||||
|
multiple
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onInput={() => loadFiles()}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
title="Clears the external step/iges/stl files stored in the project."
|
||||||
|
onClick={() => clearExternalFiles()}
|
||||||
|
>
|
||||||
|
Clear Imported Files
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
title="Resets the project and localstorage."
|
||||||
|
onClick={() => {
|
||||||
|
window.localStorage.clear()
|
||||||
|
window.history.replaceState({}, 'Cascade Studio', '?')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Project
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="cascade-container" style={{ height: 'auto' }}></div>
|
||||||
<footer>footer</footer>
|
<footer>footer</footer>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@@ -74,11 +117,7 @@ const HomePage1 = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return <MainLayout>hi</MainLayout>
|
||||||
<MainLayout>
|
|
||||||
hi
|
|
||||||
</MainLayout>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Link, routes } from '@redwoodjs/router'
|
|||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import IdePartCell from 'src/components/IdePartCell'
|
import IdePartCell from 'src/components/IdePartCell'
|
||||||
|
|
||||||
const IdePartPage = ({id}) => {
|
const IdePartPage = ({ id }) => {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<IdePartCell id={id} />
|
<IdePartCell id={id} />
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import { navigate, routes } from '@redwoodjs/router'
|
|||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import Part2Cell from 'src/components/Part2Cell'
|
import Part2Cell from 'src/components/Part2Cell'
|
||||||
|
|
||||||
|
const NewPart2Page = ({ userName }) => {
|
||||||
const NewPart2Page = ({userName}) => {
|
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
const { isAuthenticated, currentUser } = useAuth()
|
||||||
useEffect(() => {!isAuthenticated &&
|
useEffect(() => {
|
||||||
navigate(routes.home())},
|
!isAuthenticated && navigate(routes.home())
|
||||||
[currentUser])
|
}, [currentUser])
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Part2Cell
|
<Part2Cell
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAuth } from '@redwoodjs/auth'
|
|||||||
import MainLayout from 'src/layouts/MainLayout'
|
import MainLayout from 'src/layouts/MainLayout'
|
||||||
import Part2Cell from 'src/components/Part2Cell'
|
import Part2Cell from 'src/components/Part2Cell'
|
||||||
|
|
||||||
const Part2Page = ({userName, partTitle}) => {
|
const Part2Page = ({ userName, partTitle }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user