Lint project #92

Merged
Irev-Dev merged 1 commits from lint-whole-project into main 2020-11-10 20:11:11 +01:00
33 changed files with 852 additions and 481 deletions

View File

@@ -68,11 +68,12 @@ export const handler = async (req, _context) => {
const isUnique = !(await db.user.findOne({
where: { userName: seed },
}))
if(isUnique) {
if (isUnique) {
return seed
}
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)
}
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,
id: user.id,
}
await createUserInsecure({input})
await createUserInsecure({ input })
return {
statusCode: 200,

View File

@@ -16,7 +16,7 @@ export const schema = gql`
type Query {
parts: [Part!]!
part(id: String!): Part
partByUserAndTitle(userName: String! partTitle: String!): Part
partByUserAndTitle(userName: String!, partTitle: String!): Part
}
input CreatePartInput {

View File

@@ -142,7 +142,7 @@ export const requireAuth = ({ role } = {}) => {
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.")
}
}

View File

@@ -7,37 +7,38 @@ export const requireOwnership = async ({ userId, userName, partId } = {}) => {
if (!context.currentUser) {
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.")
}
if(context.currentUser.roles?.includes('admin')) {
if (context.currentUser.roles?.includes('admin')) {
return
}
const netlifyUserId = context.currentUser?.sub
if(userId && userId !== netlifyUserId) {
if (userId && userId !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
if(userName) {
if (userName) {
const user = await db.user.findOne({
where: { userName },
})
if(!user || user.id !== netlifyUserId) {
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
if(partId) {
const user = await db.part.findOne({
where: { id: partId },
}).user()
if (partId) {
const user = await db.part
.findOne({
where: { id: partId },
})
.user()
if(!user || user.id !== netlifyUserId) {
if (!user || user.id !== netlifyUserId) {
throw new ForbiddenError("You don't own this resource.")
}
}
}

View File

@@ -12,4 +12,5 @@ export const foreignKeyReplacement = (input) => {
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

@@ -18,20 +18,24 @@ export const partReaction = ({ id }) => {
export const togglePartReaction = async ({ input }) => {
// if write fails emote_userId_partId @@unique constraint, then delete it instead
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
if(!legalReactions.includes(input.emote)) {
throw new UserInputError(`You can't react with '${input.emote}', only the following are allowed: ${legalReactions.join(', ')}`)
if (!legalReactions.includes(input.emote)) {
throw new UserInputError(
`You can't react with '${
input.emote
}', only the following are allowed: ${legalReactions.join(', ')}`
)
}
let dbPromise
const inputClone = {...input} // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
try{
const inputClone = { ...input } // TODO foreignKeyReplacement mutates input, which I should fix but am lazy right now
try {
dbPromise = await db.partReaction.create({
data: foreignKeyReplacement(input),
})
} catch(e) {
} catch (e) {
dbPromise = db.partReaction.delete({
where: { emote_userId_partId: inputClone},
where: { emote_userId_partId: inputClone },
})
}
return dbPromise

View File

@@ -1,5 +1,8 @@
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 { requireOwnership } from 'src/lib/owner'
@@ -15,15 +18,15 @@ export const part = ({ id }) => {
export const partByUserAndTitle = async ({ userName, partTitle }) => {
const user = await db.user.findOne({
where: {
userName
}
userName,
},
})
return db.part.findOne({
where: {
title_userId: {
title: partTitle,
userId: user.id,
}
},
},
})
}
@@ -37,8 +40,8 @@ export const createPart = async ({ input }) => {
export const updatePart = async ({ id, input }) => {
requireAuth()
await requireOwnership({partId: id})
if(input.title) {
await requireOwnership({ partId: id })
if (input.title) {
input.title = enforceAlphaNumeric(input.title)
}
return db.part.update({
@@ -59,5 +62,7 @@ export const Part = {
Comment: (_obj, { root }) =>
db.part.findOne({ where: { id: root.id } }).Comment(),
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

@@ -23,7 +23,7 @@ export const userName = ({ userName }) => {
export const createUser = ({ input }) => {
requireAuth({ role: 'admin' })
createUserInsecure({input})
createUserInsecure({ input })
}
export const createUserInsecure = ({ input }) => {
return db.user.create({
@@ -41,12 +41,15 @@ export const updateUser = ({ id, input }) => {
export const updateUserByUserName = async ({ userName, input }) => {
requireAuth()
await requireOwnership({userName})
if(input.userName) {
await requireOwnership({ userName })
if (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
throw new UserInputError(`You've tried to used a protected word as you userName, try something other than `)
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
throw new UserInputError(
`You've tried to used a protected word as you userName, try something other than `
)
}
return db.user.update({
data: input,
@@ -63,10 +66,16 @@ export const deleteUser = ({ id }) => {
export const User = {
Parts: (_obj, { root }) => db.user.findOne({ where: { id: root.id } }).Part(),
Part: (_obj, { root, ...rest }) => _obj.partTitle && db.part.findOne({where: { title_userId: {
title: _obj.partTitle,
userId: root.id,
}}}),
Part: (_obj, { root, ...rest }) =>
_obj.partTitle &&
db.part.findOne({
where: {
title_userId: {
title: _obj.partTitle,
userId: root.id,
},
},
}),
Reaction: (_obj, { root }) =>
db.user.findOne({ where: { id: root.id } }).Reaction(),
Comment: (_obj, { root }) =>

View File

@@ -14,15 +14,7 @@
"@redwoodjs/core": "^0.19.2"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config",
"workingDirectories": [
"./api",
"./web/src/components",
"./web/src/layouts",
"./web/src/pages",
"./web/src/index.js",
"./web/src/Routes.js"
]
"extends": "@redwoodjs/eslint-config"
},
"engines": {
"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'
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
return (
<h3 className={getActiveClasses("text-2xl font-roboto", className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">.</div>
<span className={getActiveClasses({"text-gray-500": !onPartTitleChange, 'text-gray-400': onPartTitleChange})}>
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
.
</div>
<span
className={getActiveClasses({
'text-gray-500': !onPartTitleChange,
'text-gray-400': onPartTitleChange,
})}
>
{userName}
</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
value={partTitle}
onChange={onPartTitleChange}
isEditable={onPartTitleChange}
className={getActiveClasses("text-indigo-800 text-2xl", {'-ml-2': !onPartTitleChange})}
className={getActiveClasses('text-indigo-800 text-2xl', {
'-ml-2': !onPartTitleChange,
})}
/>
</h3>
)

View File

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

View File

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

View File

@@ -35,8 +35,8 @@ export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ user }) => {
const { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
onCompleted: ({updateUserByUserName}) => {
navigate(routes.user2({userName: updateUserByUserName.userName}))
onCompleted: ({ updateUserByUserName }) => {
navigate(routes.user2({ userName: updateUserByUserName.userName }))
addMessage('User updated.', { classes: 'rw-flash-success' })
},
})
@@ -45,11 +45,13 @@ export const Success = ({ user }) => {
updateUser({ variables: { userName, input } })
}
return <UserProfile
user={user}
onSave={onSave}
loading={loading}
error={error}
isEditable
/>
return (
<UserProfile
user={user}
onSave={onSave}
loading={loading}
error={error}
isEditable
/>
)
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'
import { getActiveClasses } from "get-active-classes"
import { getActiveClasses } from 'get-active-classes'
import Popover from '@material-ui/core/Popover'
import { useAuth } from '@redwoodjs/auth'
@@ -7,14 +7,21 @@ import Svg from 'src/components/Svg'
const emojiMenu = ['❤️', '👍', '😄', '🙌']
// const emojiMenu = ['🏆', '❤️', '👍', '😊', '😄', '🚀', '👏', '🙌']
const noEmotes =[{
emoji: '❤️',
count: 0,
}]
const noEmotes = [
{
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 [isOpen, setIsOpen] = useState(false)
const [anchorEl, setAnchorEl] = useState(null)
@@ -48,29 +55,40 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) =>
return (
<>
<div className={getActiveClasses("h-10 relative overflow-hidden py-4", className)}>
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
<div className="h-8 w-8 relative" aria-describedby={popoverId} onClick={togglePopover}>
<button
className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150"
>
<Svg className="h-8 w-8 pt-px mt-px text-gray-500" name="dots-vertical" />
</button>
</div>
<div
className={getActiveClasses(
'h-10 relative overflow-hidden py-4',
className
)}
>
<div className="absolute left-0 w-8 inset-y-0 z-10 flex items-center bg-gray-100">
<div
className="h-8 w-8 relative"
aria-describedby={popoverId}
onClick={togglePopover}
>
<button className="bg-gray-200 border-2 m-px w-full h-full border-gray-300 rounded-full flex justify-center items-center shadow-md hover:shadow-lg hover:border-indigo-200 transform hover:-translate-y-px transition-all duration-150">
<Svg
className="h-8 w-8 pt-px mt-px text-gray-500"
name="dots-vertical"
/>
</button>
</div>
</div>
<div className="whitespace-no-wrap absolute right-0 inset-y-0 flex items-center flex-row-reverse">
{(emotes.length ? emotes : noEmotes).map((emote, i) => (
<span
className={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",
{'border': currentUser && userEmotes?.includes(emote.emoji)}
'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) }
)}
style={textShadow}
key={`${emote.emoji}--${i}`}
onClick={() => handleEmojiClick(emote.emoji)}
>
<span className="text-lg pr-1">{emote.emoji}</span><span className="text-sm font-ropa-sans">{emote.count}</span>
<span className="text-lg pr-1">{emote.emoji}</span>
<span className="text-sm font-ropa-sans">{emote.count}</span>
</span>
))}
</div>
@@ -96,7 +114,9 @@ const EmojiReaction = ({ emotes, userEmotes, onEmote = () => {}, className }) =>
style={textShadow}
key={`${emoji}-${i}}`}
onClick={() => handleEmojiClick(emoji)}
>{emoji}</button>
>
{emoji}
</button>
))}
</div>
</Popover>

View File

@@ -35,8 +35,7 @@ export const Success = ({ part }) => {
addMessage('Part updated.', { classes: 'rw-flash-success' })
},
})
console.log({updatePart})
console.log({ updatePart })
const saveCode = (input, id) => {
console.log(id, input, 'wowow')

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
import Button from "@material-ui/core/Button";
import React, { useCallback, useState } from 'react'
import { useDropzone } from 'react-dropzone'
import Button from '@material-ui/core/Button'
import axios from 'axios'
import ReactCrop from 'react-image-crop'
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 Svg from 'src/components/Svg/Svg.js'
const CLOUDINARY_UPLOAD_PRESET = "CadHub_project_images";
const CLOUDINARY_UPLOAD_URL = "https://api.cloudinary.com/v1_1/irevdev/upload";
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
export default function ImageUploader({
onImageUpload,
imageUrl,
aspectRatio,
className,
isEditable,
width=600
}) {
onImageUpload,
imageUrl,
aspectRatio,
className,
isEditable,
width = 600,
}) {
const [isModalOpen, setIsModalOpen] = useState(false)
const [file, setFile] = useState()
const [cloudinaryId, setCloudinaryId] = useState(imageUrl)
@@ -27,17 +27,17 @@ export default function ImageUploader({
aspect: aspectRatio,
unit: '%',
width: 100,
});
})
async function handleImageUpload() {
const croppedFile = await getCroppedImg(imageObj, crop, 'avatar')
const imageData = new FormData();
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET);
imageData.append('file', croppedFile);
const imageData = new FormData()
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
imageData.append('file', croppedFile)
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
try {
const { data } = await upload
if (data && data.public_id !== "") {
onImageUpload({cloudinaryPublicId: data.public_id})
if (data && data.public_id !== '') {
onImageUpload({ cloudinaryPublicId: data.public_id })
setCloudinaryId(data.public_id)
setIsModalOpen(false)
}
@@ -46,62 +46,85 @@ export default function ImageUploader({
}
}
// Drag and Drop
const onDrop = useCallback(acceptedFiles => {
const onDrop = useCallback((acceptedFiles) => {
setIsModalOpen(true)
const fileReader = new FileReader()
fileReader.onload = () => {
setFile(fileReader.result)
}
fileReader.readAsDataURL(acceptedFiles[0])
}, []);
}, [])
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
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()}>
{cloudinaryId && isEditable && <button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
<Svg name="pencil" strokeWidth={2} className="text-gray-300 h-24 w-24" />
</button>}
{cloudinaryId && isEditable && (
<button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
<Svg
name="pencil"
strokeWidth={2}
className="text-gray-300 h-24 w-24"
/>
</button>
)}
{isEditable && <input {...getInputProps()} />}
{(cloudinaryId || !isEditable) && <div className="relative overflow-hidden w-full h-full">
<CloudinaryImage
className="object-cover w-full h-full shadow overflow-hidden"
cloudName="irevdev"
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
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>
{(cloudinaryId || !isEditable) && (
<div className="relative overflow-hidden w-full h-full">
<CloudinaryImage
className="object-cover w-full h-full shadow overflow-hidden"
cloudName="irevdev"
publicId={cloudinaryId || 'CadHub/eia1kwru54g2kf02s2xx'}
width={width}
crop="scale"
/>
</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>
<Dialog
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<Dialog open={isModalOpen} onClose={() => setIsModalOpen(false)}>
<div className="p-4">
<ReactCrop src={file} crop={crop} onImageLoaded={(image) => setImageObj(image)} onChange={newCrop => setCrop(newCrop)} />
<Button onClick={handleImageUpload} variant="outlined">Upload</Button>
<ReactCrop
src={file}
crop={crop}
onImageLoaded={(image) => setImageObj(image)}
onChange={(newCrop) => setCrop(newCrop)}
/>
<Button onClick={handleImageUpload} variant="outlined">
Upload
</Button>
</div>
</Dialog>
</div>
);
)
}
function getCroppedImg(image, crop, fileName) {
const canvas = document.createElement('canvas');
const scaleX = image.naturalWidth / image.width;
const scaleY = image.naturalHeight / image.height;
canvas.width = crop.width;
canvas.height = crop.height;
const ctx = canvas.getContext('2d');
const canvas = document.createElement('canvas')
const scaleX = image.naturalWidth / image.width
const scaleY = image.naturalHeight / image.height
canvas.width = crop.width
canvas.height = crop.height
const ctx = canvas.getContext('2d')
ctx.drawImage(
image,
crop.x * scaleX,
@@ -112,16 +135,20 @@ function getCroppedImg(image, crop, fileName) {
0,
crop.width,
crop.height
);
)
// As Base64 string
// const base64Image = canvas.toDataURL('image/jpeg');
// As a blob
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
blob.name = fileName;
resolve(blob);
}, 'image/jpeg', 1);
});
canvas.toBlob(
(blob) => {
blob.name = fileName
resolve(blob)
},
'image/jpeg',
1
)
})
}

View File

@@ -4,35 +4,39 @@ export const generated = () => {
return (
<>
<h3>AspectRatio:1, no initial image, editable</h3>
<
ImageUploader
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
<ImageUploader
onImageUpload={({ cloudinaryPublicId }) =>
console.log(cloudinaryPublicId)
}
aspectRatio={1}
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>
<
ImageUploader
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
aspectRatio={16/9}
<ImageUploader
onImageUpload={({ cloudinaryPublicId }) =>
console.log(cloudinaryPublicId)
}
aspectRatio={16 / 9}
isEditable={true}
className={"bg-red-400 rounded-xl"}
className={'bg-red-400 rounded-xl'}
imageUrl="CadHub/inakek2urbreynblzhgt"
/>
<h3>AspectRatio:1, no initial image, NOT editable</h3>
<
ImageUploader
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
<ImageUploader
onImageUpload={({ cloudinaryPublicId }) =>
console.log(cloudinaryPublicId)
}
aspectRatio={1}
className={"rounded-half rounded-br-xl"}
className={'rounded-half rounded-br-xl'}
/>
<h3>AspectRatio ,16:9 no initial image, NOT editable</h3>
<
ImageUploader
onImageUpload={({cloudinaryPublicId}) => console.log(cloudinaryPublicId)}
aspectRatio={16/9}
className={"rounded-xl"}
<ImageUploader
onImageUpload={({ cloudinaryPublicId }) =>
console.log(cloudinaryPublicId)
}
aspectRatio={16 / 9}
className={'rounded-xl'}
imageUrl="CadHub/inakek2urbreynblzhgt"
/>
</>

View File

@@ -1,9 +1,15 @@
import { getActiveClasses } from 'get-active-classes'
const InputText = ({value, isEditable, onChange ,className}) => {
const InputText = ({ value, isEditable, onChange, className }) => {
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" />
<input
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"
/>
</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'
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) {
id
name
@@ -42,7 +46,7 @@ export const QUERY = gql`
const UPDATE_PART_MUTATION = gql`
mutation UpdatePartMutation($id: String!, $input: UpdatePartInput!) {
updatePart:updatePart(id: $id, input: $input) {
updatePart: updatePart(id: $id, input: $input) {
id
title
user {
@@ -66,7 +70,7 @@ const CREATE_PART_MUTATION = gql`
`
const TOGGLE_REACTION_MUTATION = gql`
mutation ToggleReactionMutation($input: TogglePartReactionInput!) {
togglePartReaction(input: $input){
togglePartReaction(input: $input) {
id
emote
}
@@ -87,23 +91,33 @@ export const Empty = () => <div>Empty</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 { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
onCompleted: ({updatePart}) => {
navigate(routes.part2({userName: updatePart.user.userName, partTitle: updatePart.title}))
onCompleted: ({ updatePart }) => {
navigate(
routes.part2({
userName: updatePart.user.userName,
partTitle: updatePart.title,
})
)
addMessage('Part updated.', { classes: 'rw-flash-success' })
},
})
const [createUser] = useMutation(CREATE_PART_MUTATION, {
onCompleted: ({createPart}) => {
navigate(routes.part2({userName: createPart?.user?.userName, partTitle: createPart?.title}))
onCompleted: ({ createPart }) => {
navigate(
routes.part2({
userName: createPart?.user?.userName,
partTitle: createPart?.title,
})
)
addMessage('Part Created.', { classes: 'rw-flash-success' })
},
})
const onSave = (id, input) => {
if(!id) {
if (!id) {
createUser({ variables: { input } })
return
}
@@ -111,30 +125,42 @@ export const Success = ({ userPart, variables: {isEditable}, refetch}) => {
}
const [toggleReaction] = useMutation(TOGGLE_REACTION_MUTATION, {
onCompleted: () => refetch()
onCompleted: () => refetch(),
})
const onReaction = (emote) => toggleReaction({variables: {input: {
emote,
userId: currentUser.sub,
partId: userPart?.Part?.id,
}}})
const onReaction = (emote) =>
toggleReaction({
variables: {
input: {
emote,
userId: currentUser.sub,
partId: userPart?.Part?.id,
},
},
})
const [createComment] = useMutation(CREATE_COMMENT_MUTATION, {
onCompleted: () => refetch()
onCompleted: () => refetch(),
})
const onComment = (text) => createComment({variables: {input: {
text,
userId: currentUser.sub,
partId: userPart?.Part?.id,
}}})
const onComment = (text) =>
createComment({
variables: {
input: {
text,
userId: currentUser.sub,
partId: userPart?.Part?.id,
},
},
})
return <PartProfile
userPart={userPart}
onSave={onSave}
loading={loading}
error={error}
isEditable={isEditable}
onReaction={onReaction}
onComment={onComment}
/>
return (
<PartProfile
userPart={userPart}
onSave={onSave}
loading={loading}
error={error}
isEditable={isEditable}
onReaction={onReaction}
onComment={onComment}
/>
)
}

View File

@@ -1,58 +1,69 @@
import {useState, useEffect} from 'react'
import { useState, useEffect } from 'react'
import { useAuth } from '@redwoodjs/auth'
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 Breadcrumb from 'src/components/Breadcrumb'
import EmojiReaction from 'src/components/EmojiReaction'
import Button from 'src/components/Button'
import { countEmotes } from 'src/helpers/emote'
import { getActiveClasses } from 'get-active-classes';
import { getActiveClasses } from 'get-active-classes'
const PartProfile = ({
userPart,
isEditable,
onSave,
loading,
error,
onReaction,
onComment,
}) => {
userPart,
isEditable,
onSave,
loading,
error,
onReaction,
onComment,
}) => {
const [comment, setComment] = useState('')
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === userPart.id
const part = userPart?.Part
const emotes = countEmotes(part?.Reaction)
const userEmotes = part?.userReactions.map(({emote}) => emote)
useEffect(() => {isEditable &&
!canEdit &&
navigate(routes.part2({userName: userPart.userName, partTitle: part.title}))},
[currentUser])
const userEmotes = part?.userReactions.map(({ emote }) => emote)
useEffect(() => {
isEditable &&
!canEdit &&
navigate(
routes.part2({ userName: userPart.userName, partTitle: part.title })
)
}, [currentUser])
const [input, setInput] = useState({
title: part?.title,
mainImage: part?.mainImage,
description: part?.description,
userId: userPart?.id,
})
const setProperty = (property, value) => setInput({
...input,
[property]: value,
})
const onTitleChange = ({target}) => setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
const onDescriptionChange = (description) => setProperty('description', description())
const onImageUpload = ({cloudinaryPublicId}) => setProperty('mainImage', cloudinaryPublicId)
const setProperty = (property, value) =>
setInput({
...input,
[property]: value,
})
const onTitleChange = ({ target }) =>
setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
const onDescriptionChange = (description) =>
setProperty('description', description())
const onImageUpload = ({ cloudinaryPublicId }) =>
setProperty('mainImage', cloudinaryPublicId)
const onEditSaveClick = () => {
if (isEditable) {
input.title && onSave(part?.id, input)
return
}
navigate(routes.editPart2({userName: userPart.userName, partTitle: part.title}))
navigate(
routes.editPart2({ userName: userPart.userName, partTitle: part.title })
)
}
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 */}
<aside className="col-start-2 relative">
<ImageUploader
@@ -62,7 +73,11 @@ const PartProfile = ({
imageUrl={userPart.image}
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" />
<EmojiReaction
emotes={emotes}
@@ -85,29 +100,43 @@ const PartProfile = ({
>
Open IDE
</Button>
{canEdit && <Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
shouldAnimateHover
iconName={isEditable ? 'save' : 'pencil'}
onClick={onEditSaveClick}
>
{isEditable ? 'Save Details' : 'Edit Details'}
</Button>}
{isEditable && <div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />}
{canEdit && (
<Button
className="mt-4 ml-auto shadow-md hover:shadow-lg bg-indigo-200 relative z-20"
shouldAnimateHover
iconName={isEditable ? 'save' : 'pencil'}
onClick={onEditSaveClick}
>
{isEditable ? 'Save Details' : 'Edit Details'}
</Button>
)}
{isEditable && (
<div className="absolute inset-0 bg-gray-300 opacity-75 z-10 transform scale-x-110 -ml-1 -mt-2" />
)}
</aside>
{/* main project center column */}
<section className="col-start-3">
<Breadcrumb 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"
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">
<Breadcrumb
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"
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
defaultValue={part?.description || ''}
readOnly={!isEditable}
@@ -115,56 +144,66 @@ const PartProfile = ({
/>
</div>
{/* comments */}
{ !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>
{!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>
<ul>
{part?.Comment.map(({text, user, id}) => (
<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">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
width={50}
<ul>
{part?.Comment.map(({ text, user, id }) => (
<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">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
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 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>
<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>
</>}
</>}
<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>
</div>
</>
)

View File

@@ -4,14 +4,19 @@ import { countEmotes } from 'src/helpers/emote'
import ImageUploader from 'src/components/ImageUploader'
const PartsList = ({ parts }) => {
return (
<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))'}}>
{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}`}>
<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 }) => (
<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
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="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow">
@@ -23,7 +28,9 @@ const PartsList = ({ parts }) => {
width={50}
/>
</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 className="w-full overflow-hidden relative rounded-b-lg">
<ImageUploader
@@ -33,18 +40,27 @@ const PartsList = ({ parts }) => {
imageUrl={mainImage}
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 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 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">
{count}
</div>
</div>
<div className="text-sm pl-1 font-ropa-sans text-gray-800">
{count}
</div>
</div>
))}</div>
))}
</div>
</Link>
</li>
))}

View File

@@ -1,22 +1,30 @@
import {Fragment} from 'react'
import { Fragment } from 'react'
import InputText from 'src/components/InputText'
const ProfileTextInput = ({fields, isEditable, onChange= () => {}}) => {
const ProfileTextInput = ({ fields, isEditable, onChange = () => {} }) => {
return (
<div>
<div className="grid items-center" 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>
<div
className="grid items-center"
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
className="text-xl"
value={value}
onChange={({target}) => onChange({...fields, [property]: target.value})}
isEditable={isEditable}
/>
</Fragment>))}
<InputText
className="text-xl"
value={value}
onChange={({ target }) =>
onChange({ ...fields, [property]: target.value })
}
isEditable={isEditable}
/>
</Fragment>
))}
</div>
</div>
)

View File

@@ -1,32 +1,113 @@
const Svg = ({name, className: className2, strokeWidth = 2}) => {
const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
const svgs = {
"chevron-down": <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="M19 9l-7 7-7-7" />
</svg>,
"dots-vertical": <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 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>,
'chevron-down': (
<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="M19 9l-7 7-7-7"
/>
</svg>
),
'dots-vertical': (
<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 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"}>
{svgs[name]}
</div>
return <div className={className2 || 'h-10 w-10'}>{svgs[name]}</div>
}
export default Svg

View File

@@ -18,6 +18,6 @@ export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>User not found</div>
export const Success = ({user}) => {
export const Success = ({ user }) => {
return <UserProfile user={user} />
}

View File

@@ -1,39 +1,41 @@
import {useState, useEffect} from 'react'
import { useState, useEffect } from 'react'
import { useAuth } from '@redwoodjs/auth'
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 Button from 'src/components/Button'
import ProfileTextInput from 'src/components/ProfileTextInput'
const UserProfile = ({user, isEditable, loading, onSave, error}) => {
const UserProfile = ({ user, isEditable, loading, onSave, error }) => {
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === user.id
useEffect(() => {isEditable &&
!canEdit &&
navigate(routes.user2({userName: user.userName}))},
[currentUser])
useEffect(() => {
isEditable &&
!canEdit &&
navigate(routes.user2({ userName: user.userName }))
}, [currentUser])
const [input, setInput] = useState({
userName: user.userName,
name: user.name,
bio: user.bio,
image: user.image,
})
const {userName, name} = input
const editableTextFields = {userName, name}
const { userName, name } = input
const editableTextFields = { userName, name }
return (
<>
<section className="max-w-2xl mx-auto mt-20 ">
<div className="flex" >
<div className="flex">
<div className="w-40 flex-shrink-0">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={({cloudinaryPublicId: image}) => setInput({
...input,
image,
})}
onImageUpload={({ cloudinaryPublicId: image }) =>
setInput({
...input,
image,
})
}
aspectRatio={1}
isEditable={isEditable}
imageUrl={user.image}
@@ -41,29 +43,53 @@ const UserProfile = ({user, isEditable, loading, onSave, error}) => {
/>
</div>
<div className="ml-6 flex flex-col justify-between">
<ProfileTextInput fields={editableTextFields} onChange={({userName, name}) => setInput({
...input,
name,
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
}
<ProfileTextInput
fields={editableTextFields}
onChange={({ userName, name }) =>
setInput({
...input,
name,
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}
</div>
</div>
<div className="mt-10">
<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
defaultValue={user.bio || ''}
readOnly={!isEditable}
onChange={(bioFn) => setInput({
...input,
bio: bioFn(),
})}
onChange={(bioFn) =>
setInput({
...input,
bio: bioFn(),
})
}
/>
</div>
</div>

View File

@@ -2,9 +2,12 @@ export const countEmotes = (reactions = []) => {
// would be good to do this sever side
// counting unique emojis, and limiting to the 5 largest
const emoteCounts = {}
reactions.forEach(({emote}) => {
reactions.forEach(({ emote }) => {
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
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

@@ -1,11 +1,11 @@
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 { Flash } from '@redwoodjs/web'
import Tooltip from '@material-ui/core/Tooltip';
import Tooltip from '@material-ui/core/Tooltip'
import { useQuery } from '@redwoodjs/web'
import Popover from '@material-ui/core/Popover'
import {getActiveClasses} from 'get-active-classes'
import { getActiveClasses } from 'get-active-classes'
export const QUERY = gql`
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 ImageUploader from 'src/components/ImageUploader'
import logo from 'src/layouts/MainLayout/Logo_2.jpg'
const MainLayout = ({ children}) => {
const MainLayout = ({ children }) => {
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 [anchorEl, setAnchorEl] = useState(null)
const [popoverId, setPopoverId] = useState(undefined)
@@ -55,38 +56,59 @@ const MainLayout = ({ children}) => {
<li>
<Link to={routes.home()}>
<div className="rounded-full overflow-hidden ml-12">
<img src={logo}/>
<img src={logo} />
</div>
</Link>
</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">
{/* 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>
<div className="text-pink-400 text-sm font-bold font-ropa-sans" style={{paddingBottom: '2rem', marginLeft: '-1.8rem'}}>pre-alpha</div>
<h2
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>
</Tooltip>
</li>
</ul>
<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})}>
{isAuthenticated && data?.user?.userName ?
<Link className="h-full w-full" to={routes.newPart2({userName: data?.user?.userName})}>
<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 }
)}
>
{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" />
</Link>:
</Link>
) : (
<Svg name="plus" className="text-gray-700 w-full h-full" />
}
)}
</li>
<li className="h-10 w-10 border-1 rounded-full border-indigo-300 text-indigo-200">
<div aria-describedby={popoverId} onMouseOver={togglePopover}>
{!loading && <ImageUploader
className="rounded-full object-cover"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={data?.user?.image}
width={80}
/>}
<div aria-describedby={popoverId} onMouseOver={togglePopover}>
{!loading && (
<ImageUploader
className="rounded-full object-cover"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={data?.user?.image}
width={80}
/>
)}
</div>
</li>
</ul>
@@ -104,23 +126,33 @@ const MainLayout = ({ children}) => {
horizontal: 'right',
}}
>
{
isAuthenticated && currentUser?
<div style={{padding: '1em', width: '15em'}} >
<Link to={routes.user2({userName: data?.user?.userName})}>
<h3 className="text-indigo-800" style={{fontWeight: '500'}} >Hello {data?.user?.name}</h3>
</Link>
<hr/>
<br/>
<Link to={routes.editUser2({userName: data?.user?.userName})}>
<div className="text-indigo-800" >Edit Profile</div>
</Link>
<a href="#" className="text-indigo-800" onClick={logOut}>Logout</a>
</div>:
<div style={{padding: '1em', width: '15em'}} >
<a href="#" className="text-indigo-800 text-indigo-800" onClick={logIn}>LOGIN/SIGNUP</a>
</div>
}
{isAuthenticated && currentUser ? (
<div style={{ padding: '1em', width: '15em' }}>
<Link to={routes.user2({ userName: data?.user?.userName })}>
<h3 className="text-indigo-800" style={{ fontWeight: '500' }}>
Hello {data?.user?.name}
</h3>
</Link>
<hr />
<br />
<Link to={routes.editUser2({ userName: data?.user?.userName })}>
<div className="text-indigo-800">Edit Profile</div>
</Link>
<a href="#" className="text-indigo-800" onClick={logOut}>
Logout
</a>
</div>
) : (
<div style={{ padding: '1em', width: '15em' }}>
<a
href="#"
className="text-indigo-800 text-indigo-800"
onClick={logIn}
>
LOGIN/SIGNUP
</a>
</div>
)}
</Popover>
</nav>
</header>

View File

@@ -3,7 +3,7 @@ import { useAuth } from '@redwoodjs/auth'
import MainLayout from 'src/layouts/MainLayout'
import Part2Cell from 'src/components/Part2Cell'
const EditPart2Page = ({userName, partTitle}) => {
const EditPart2Page = ({ userName, partTitle }) => {
const { currentUser } = useAuth()
return (
<MainLayout>

View File

@@ -4,7 +4,7 @@ import EditUser2Cell from 'src/components/EditUser2Cell'
const UserPage = ({ userName }) => {
return (
<MainLayout>
<EditUser2Cell userName={userName}/>
<EditUser2Cell userName={userName} />
</MainLayout>
)
}

View File

@@ -3,8 +3,7 @@ import MainLayout from 'src/layouts/MainLayout'
import { initialize } from 'src/cascade/js/MainPage/CascadeMain'
import { useEffect, useState } from 'react'
const starterCode =
`// Welcome to Cascade Studio! Here are some useful functions:
const starterCode = `// Welcome to Cascade Studio! Here are some useful functions:
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
// Box(), Sphere(), Cylinder(), Cone(), Text3D(), Polygon()
// 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"));
// Don't forget to push imported or oc-defined shapes into sceneShapes to add them to the workspace!
`;
`
const HomePage1 = () => {
const [code, setCode] = useState(starterCode)
@@ -34,39 +33,83 @@ const HomePage1 = () => {
new initialize(sickCallback, starterCode)
}, [])
return (
<MainLayout>
<div>current code {code}</div>
<BlogPostsCell/>
<BlogPostsCell />
<div>
<div id="topnav" className="topnav">
<a href="https://github.com/zalo/CascadeStudio">Cascade Studio 0.0.6</a>
<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
id="project-file"
name="project-file"
type="file"
accept=".json"
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 id="cascade-container" style={{height:'auto'}}>
<a href="https://github.com/zalo/CascadeStudio">
Cascade Studio 0.0.6
</a>
<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
id="project-file"
name="project-file"
type="file"
accept=".json"
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 id="cascade-container" style={{ height: 'auto' }}></div>
<footer>footer</footer>
</div>
</MainLayout>
@@ -74,11 +117,7 @@ const HomePage1 = () => {
}
const HomePage = () => {
return (
<MainLayout>
hi
</MainLayout>
)
return <MainLayout>hi</MainLayout>
}
export default HomePage

View File

@@ -2,7 +2,7 @@ import { Link, routes } from '@redwoodjs/router'
import MainLayout from 'src/layouts/MainLayout'
import IdePartCell from 'src/components/IdePartCell'
const IdePartPage = ({id}) => {
const IdePartPage = ({ id }) => {
return (
<MainLayout>
<IdePartCell id={id} />

View File

@@ -5,12 +5,11 @@ import { navigate, routes } from '@redwoodjs/router'
import MainLayout from 'src/layouts/MainLayout'
import Part2Cell from 'src/components/Part2Cell'
const NewPart2Page = ({userName}) => {
const NewPart2Page = ({ userName }) => {
const { isAuthenticated, currentUser } = useAuth()
useEffect(() => {!isAuthenticated &&
navigate(routes.home())},
[currentUser])
useEffect(() => {
!isAuthenticated && navigate(routes.home())
}, [currentUser])
return (
<MainLayout>
<Part2Cell

View File

@@ -3,7 +3,7 @@ import { useAuth } from '@redwoodjs/auth'
import MainLayout from 'src/layouts/MainLayout'
import Part2Cell from 'src/components/Part2Cell'
const Part2Page = ({userName, partTitle}) => {
const Part2Page = ({ userName, partTitle }) => {
const { currentUser } = useAuth()
return (
<MainLayout>