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:
Kurt Hutten
2020-11-11 06:11:11 +11:00
committed by GitHub
33 changed files with 852 additions and 481 deletions

View File

@@ -72,7 +72,8 @@ export const handler = async (req, _context) => {
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])

View File

@@ -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 {

View File

@@ -31,13 +31,14 @@ export const requireOwnership = async ({ userId, userName, partId } = {}) => {
} }
if (partId) { if (partId) {
const user = await db.part.findOne({ const user = await db.part
.findOne({
where: { id: partId }, where: { id: partId },
}).user() })
.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.")
} }
} }
} }

View File

@@ -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, '-')

View File

@@ -21,7 +21,11 @@ export const togglePartReaction = async ({ input }) => {
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

View File

@@ -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,
} },
}, },
}) })
} }
@@ -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 } }),
} }

View File

@@ -45,8 +45,11 @@ export const updateUserByUserName = async ({ userName, input }) => {
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 }) =>
_obj.partTitle &&
db.part.findOne({
where: {
title_userId: {
title: _obj.partTitle, title: _obj.partTitle,
userId: root.id, 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 }) =>

View File

@@ -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",

View File

@@ -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>
) )

View File

@@ -1,15 +1,25 @@
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}, {
'mx-px transform hover:-translate-y-px transition-all duration-150':
shouldAnimateHover && !disabled,
},
className, className,
{"bg-gray-300 shadow-none hover:shadow-none": disabled}, { 'bg-gray-300 shadow-none hover:shadow-none': disabled }
)} )}
onClick={onClick} onClick={onClick}
> >

View File

@@ -1,10 +1,12 @@
import Button from './Button' import Button from './Button'
export const generated = () => { export const generated = () => {
return <> return (
<>
button with icon button with icon
<Button>click Me </Button> <Button>click Me </Button>
</> </>
)
} }
export default { title: 'Components/Button' } export default { title: 'Components/Button' }

View File

@@ -45,11 +45,13 @@ export const Success = ({ user }) => {
updateUser({ variables: { userName, input } }) updateUser({ variables: { userName, input } })
} }
return <UserProfile return (
<UserProfile
user={user} user={user}
onSave={onSave} onSave={onSave}
loading={loading} loading={loading}
error={error} error={error}
isEditable isEditable
/> />
)
} }

View File

@@ -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: '❤️', emoji: '❤️',
count: 0, 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,13 +55,23 @@ 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">
<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> </button>
</div> </div>
</div> </div>
@@ -63,14 +80,15 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) =>
{(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>

View File

@@ -37,7 +37,6 @@ export const Success = ({ part }) => {
}) })
console.log({ updatePart }) console.log({ updatePart })
const saveCode = (input, id) => { const saveCode = (input, id) => {
console.log(id, input, 'wowow') console.log(id, input, 'wowow')
updatePart({ variables: { id, input } }) updatePart({ variables: { id, input } })

View File

@@ -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,8 +8,8 @@ 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,
@@ -17,7 +17,7 @@ export default function ImageUploader({
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()
@@ -27,16 +27,16 @@ 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,24 +46,38 @@ 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) && (
<div className="relative overflow-hidden w-full h-full">
<CloudinaryImage <CloudinaryImage
className="object-cover w-full h-full shadow overflow-hidden" className="object-cover w-full h-full shadow overflow-hidden"
cloudName="irevdev" cloudName="irevdev"
@@ -71,37 +85,46 @@ export default function ImageUploader({
width={width} width={width}
crop="scale" crop="scale"
/> />
</div>} </div>
)}
{!cloudinaryId && <button className="absolute inset-0"></button>} {!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"> {!cloudinaryId && isEditable && (
<div className="text-indigo-500 flex items-center justify-center rounded-lg w-full h-full">
<div className="px-6 text-center"> <div className="px-6 text-center">
Drop files here ... Drop files here ... or{' '}
or <span className="group flex w-full items-center justify-center py-2"> <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 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> </span>
</div> </div>
</div>}
</div> </div>
<Dialog )}
open={isModalOpen} </div>
onClose={() => setIsModalOpen(false)} <Dialog 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
)
})
} }

View File

@@ -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} aspectRatio={16 / 9}
className={"rounded-xl"} className={'rounded-xl'}
imageUrl="CadHub/inakek2urbreynblzhgt" imageUrl="CadHub/inakek2urbreynblzhgt"
/> />
</> </>

View File

@@ -3,7 +3,13 @@ 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>
</> </>
) )
} }

View File

@@ -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
@@ -92,13 +96,23 @@ export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
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' })
}, },
}) })
@@ -111,24 +125,35 @@ 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) =>
toggleReaction({
variables: {
input: {
emote, emote,
userId: currentUser.sub, userId: currentUser.sub,
partId: userPart?.Part?.id, 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) =>
createComment({
variables: {
input: {
text, text,
userId: currentUser.sub, userId: currentUser.sub,
partId: userPart?.Part?.id, partId: userPart?.Part?.id,
}}}) },
},
})
return <PartProfile return (
<PartProfile
userPart={userPart} userPart={userPart}
onSave={onSave} onSave={onSave}
loading={loading} loading={loading}
@@ -137,4 +162,5 @@ export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
onReaction={onReaction} onReaction={onReaction}
onComment={onComment} onComment={onComment}
/> />
)
} }

View File

@@ -1,14 +1,14 @@
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,
@@ -25,34 +25,45 @@ const PartProfile = ({
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(() => {
isEditable &&
!canEdit && !canEdit &&
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))}, navigate(
[currentUser]) 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) =>
setInput({
...input, ...input,
[property]: value, [property]: value,
}) })
const onTitleChange = ({target}) => setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-')) const onTitleChange = ({ target }) =>
const onDescriptionChange = (description) => setProperty('description', description()) setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
const onImageUpload = ({cloudinaryPublicId}) => setProperty('mainImage', cloudinaryPublicId) 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 && (
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20" className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
shouldAnimateHover shouldAnimateHover
iconName={isEditable ? 'save' : 'pencil'} iconName={isEditable ? 'save' : 'pencil'}
onClick={onEditSaveClick} onClick={onEditSaveClick}
> >
{isEditable ? 'Save Details' : 'Edit Details'} {isEditable ? 'Save Details' : 'Edit Details'}
</Button>} </Button>
{isEditable && <div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />} )}
{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"
onPartTitleChange={isEditable && onTitleChange}
userName={userPart.userName}
partTitle={input?.title}
/>
{!!(input?.mainImage || isEditable) && (
<ImageUploader
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8" className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
onImageUpload={onImageUpload} onImageUpload={onImageUpload}
aspectRatio={16 / 9} aspectRatio={16 / 9}
isEditable={isEditable} isEditable={isEditable}
imageUrl={input?.mainImage} imageUrl={input?.mainImage}
width={1010} width={1010}
/>} />
<div name="description" className="markdown-overrides rounded-lg shadow-md bg-white p-12 my-8 min-h-md"> )}
<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,12 +144,13 @@ const PartProfile = ({
/> />
</div> </div>
{/* comments */} {/* comments */}
{ !isEditable && <> {!isEditable && (
<>
<div className="h-px bg-indigo-200 mt-8" /> <div className="h-px bg-indigo-200 mt-8" />
<h3 className="text-indigo-800 text-lg font-roboto tracking-wider mb-4" >Comments</h3> <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 }) => (
@@ -135,9 +165,11 @@ const PartProfile = ({
/> />
</div> </div>
<div className="ml-4 font-roboto"> <div className="ml-4 font-roboto">
<div className="text-gray-800 font-bold text-lg mb-1"><Link to={routes.user2({userName: user.userName})}> <div className="text-gray-800 font-bold text-lg mb-1">
<Link to={routes.user2({ userName: user.userName })}>
{user.userName} {user.userName}
</Link></div> </Link>
</div>
<div className="text-gray-700 p-3 rounded bg-gray-200 shadow"> <div className="text-gray-700 p-3 rounded bg-gray-200 shadow">
{text} {text}
</div> </div>
@@ -145,7 +177,8 @@ const PartProfile = ({
</li> </li>
))} ))}
</ul> </ul>
{currentUser && <> {currentUser && (
<>
<div className="mt-12 ml-12"> <div className="mt-12 ml-12">
<textarea <textarea
className="w-full h-32 rounded-lg shadow-inner outline-none resize-none p-3" className="w-full h-32 rounded-lg shadow-inner outline-none resize-none p-3"
@@ -155,16 +188,22 @@ const PartProfile = ({
/> />
</div> </div>
<Button <Button
className={getActiveClasses("ml-auto hover:shadow-lg bg-gray-300 mt-4 mb-20", {'bg-indigo-200': currentUser})} className={getActiveClasses(
'ml-auto hover:shadow-lg bg-gray-300 mt-4 mb-20',
{ 'bg-indigo-200': currentUser }
)}
shouldAnimateHover shouldAnimateHover
disabled={!currentUser} disabled={!currentUser}
iconName={'save'} iconName={'save'}
onClick={() => onComment(comment)} onClick={() => onComment(comment)}
>Comment</Button> >
</>} Comment
</>} </Button>
</>
)}
</>
)}
</section> </section>
</div> </div>
</> </>
) )

View File

@@ -4,12 +4,17 @@ 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
className="grid gap-x-8 gap-y-12 items-center mx-4 relative"
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(16rem, 1fr))' }}
>
{parts.map(({ title, mainImage, user, Reaction }) => ( {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}`}> <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 })}
> >
@@ -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
</div> className="absolute inset-0"
<div className="absolute inset-x-0 bottom-0 -mb-4 mr-4 flex justify-end">{countEmotes(Reaction).map(({emoji, count}) => ( style={{
<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"> background:
<div className="-ml-px text-sm w-1"> 'linear-gradient(19.04deg, rgba(62, 44, 118, 0.46) 10.52%, rgba(60, 54, 107, 0) 40.02%)',
{emoji} }}
/>
</div> </div>
<div className="absolute inset-x-0 bottom-0 -mb-4 mr-4 flex justify-end">
{countEmotes(Reaction).map(({ emoji, count }) => (
<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"
>
<div className="-ml-px text-sm w-1">{emoji}</div>
<div className="text-sm pl-1 font-ropa-sans text-gray-800"> <div className="text-sm pl-1 font-ropa-sans text-gray-800">
{count} {count}
</div> </div>
</div> </div>
))}</div> ))}
</div>
</Link> </Link>
</li> </li>
))} ))}

View File

@@ -5,18 +5,26 @@ 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 }) =>
onChange({ ...fields, [property]: target.value })
}
isEditable={isEditable} isEditable={isEditable}
/> />
</Fragment>
</Fragment>))} ))}
</div> </div>
</div> </div>
) )

View File

@@ -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

View File

@@ -1,20 +1,20 @@
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(() => {
isEditable &&
!canEdit && !canEdit &&
navigate(routes.user2({userName: user.userName}))}, navigate(routes.user2({ userName: user.userName }))
[currentUser]) }, [currentUser])
const [input, setInput] = useState({ const [input, setInput] = useState({
userName: user.userName, userName: user.userName,
name: user.name, name: user.name,
@@ -30,10 +30,12 @@ const UserProfile = ({user, isEditable, loading, onSave, error}) => {
<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 }) =>
setInput({
...input, ...input,
image, 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
fields={editableTextFields}
onChange={({ userName, name }) =>
setInput({
...input, ...input,
name, name,
userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'), userName: userName.replace(/([^a-zA-Z\d_:])/g, '-'),
})} 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
} }
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) =>
setInput({
...input, ...input,
bio: bioFn(), bio: bioFn(),
})} })
}
/> />
</div> </div>
</div> </div>

View File

@@ -6,5 +6,8 @@ export const countEmotes = (reactions = []) => {
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)
} }

View File

@@ -2,7 +2,7 @@ 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'
@@ -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)
@@ -63,30 +64,51 @@ const MainLayout = ({ children}) => {
<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 && (
<ImageUploader
className="rounded-full object-cover" className="rounded-full object-cover"
onImageUpload={() => {}} onImageUpload={() => {}}
aspectRatio={1} aspectRatio={1}
imageUrl={data?.user?.image} imageUrl={data?.user?.image}
width={80} 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'}} >Hello {data?.user?.name}</h3> <h3 className="text-indigo-800" style={{ fontWeight: '500' }}>
Hello {data?.user?.name}
</h3>
</Link> </Link>
<hr /> <hr />
<br /> <br />
<Link to={routes.editUser2({ userName: data?.user?.userName })}> <Link to={routes.editUser2({ userName: data?.user?.userName })}>
<div className="text-indigo-800">Edit Profile</div> <div className="text-indigo-800">Edit Profile</div>
</Link> </Link>
<a href="#" className="text-indigo-800" onClick={logOut}>Logout</a> <a href="#" className="text-indigo-800" onClick={logOut}>
</div>: Logout
<div style={{padding: '1em', width: '15em'}} > </a>
<a href="#" className="text-indigo-800 text-indigo-800" onClick={logIn}>LOGIN/SIGNUP</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>

View File

@@ -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,16 +33,31 @@ 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
href="#"
id="main-proj-button"
title="Sets this project to save in local storage."
onClick={() => makeMainProject()}
>
Make Main Project
</a>
<a
href="#"
title="Save Project to .json"
onClick={() => saveProject()}
>
Save Project
</a>
<label htmlFor="project-file" title="Load Project from .json">
Load Project
<input <input
id="project-file" id="project-file"
name="project-file" name="project-file"
@@ -53,20 +67,49 @@ const HomePage1 = () => {
onInput={() => loadProject()} onInput={() => loadProject()}
/> />
</label> </label>
<a href="#" onClick={() => threejsViewport.saveShapeSTEP()}>Save STEP</a> <a href="#" onClick={() => threejsViewport.saveShapeSTEP()}>
<a href="#" onClick={() => threejsViewport.saveShapeSTL()}>Save STL</a> Save STEP
<a href="#" onClick={() => threejsViewport.saveShapeOBJ()}>Save OBJ</a> </a>
<label htmlFor="files" title="Import STEP, IGES, or (ASCII) STL from File">Import STEP/IGES/STL <a href="#" onClick={() => threejsViewport.saveShapeSTL()}>
<input id="files" name="files" type="file" accept=".iges,.step,.igs,.stp,.stl" multiple style={{display: 'none'}} onInput={ () =>loadFiles()}/> 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> </label>
<a href="#" title="Clears the external step/iges/stl files stored in the project." onClick={() => clearExternalFiles()}>Clear Imported Files</a> <a
<a href="" title="Resets the project and localstorage." onClick={() => { href="#"
window.localStorage.clear(); 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', '?') window.history.replaceState({}, 'Cascade Studio', '?')
}}>Reset Project</a> }}
</div> >
<div id="cascade-container" style={{height:'auto'}}> 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

View File

@@ -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