Merge branch 'dev' into feature/list-your-product-ui

This commit is contained in:
MTG2000
2022-09-15 13:25:06 +03:00
74 changed files with 2691 additions and 792 deletions

View File

@@ -10,17 +10,12 @@ import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
import { getPropertyFromUnknown, trimText, } from "src/utils/helperFunctions";
import { fetchIsLoggedIn, fetchLnurlAuth } from "src/api/auth";
import { useErrorHandler } from 'react-error-boundary';
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url', {
credentials: 'include'
})
const data = await res.json()
return data;
}
export const useLnurlQuery = () => {
const [loading, setLoading] = useState(true)
@@ -102,15 +97,9 @@ export default function LoginPage() {
if (canFetchIsLogged.current === false) return;
canFetchIsLogged.current = false;
fetch(CONSTS.apiEndpoint + '/is-logged-in', {
credentials: 'include',
headers: {
session_token
}
})
.then(data => data.json())
.then(data => {
if (data.logged_in) {
fetchIsLoggedIn(session_token)
.then(is_logged_in => {
if (is_logged_in) {
clearInterval(interval)
refetch();
}

View File

@@ -2,8 +2,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form";
import Button from "src/Components/Button/Button";
import DatePicker from "src/Components/Inputs/DatePicker/DatePicker";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import { Tag } from "src/graphql";
import { imageSchema } from "src/utils/validation";
import * as yup from "yup";
import ContentEditor from "../ContentEditor/ContentEditor";
@@ -31,29 +32,14 @@ const schema = yup.object({
.string()
.required()
.min(50, 'you have to write at least 10 words'),
cover_image: yup
.lazy((value: string | File[]) => {
switch (typeof value) {
case 'object':
return yup
.array()
.test("fileSize", "File Size is too large", (files) => (files as File[]).every(file => file.size <= 5242880))
.test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed",
(files) => (files as File[]).every((file: File) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type)))
case 'string':
return yup.string().url();
default:
return yup.mixed()
}
})
cover_image: imageSchema,
}).required();
interface IFormInputs {
title: string
deadline: Date
bounty_amount: number
tags: NestedValue<object[]>
tags: NestedValue<Tag[]>
cover_image: NestedValue<File[]> | string
body: string
}
@@ -86,7 +72,7 @@ export default function BountyForm() {
<div
className='bg-white shadow-lg rounded-8 overflow-hidden'>
<div className="p-32">
<Controller
{/* <Controller
control={control}
name="cover_image"
render={({ field: { onChange, value, onBlur } }) => (
@@ -97,7 +83,7 @@ export default function BountyForm() {
uploadText='Add a cover image'
/>
)}
/>
/> */}
<p className='input-error'>{errors.cover_image?.message}</p>
@@ -155,10 +141,20 @@ export default function BountyForm() {
<p className="text-body5 mt-16">
Tags
</p>
<TagsInput
placeholder="Enter your tag and click enter. You can add multiple tags to your post"
classes={{ container: 'mt-8' }}
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput
placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
)}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}
</p>}

View File

@@ -14,7 +14,6 @@ export default {
decorators: [WithModals, WrapForm<IStoryFormInputs>({
defaultValues: {
tags: [],
cover_image: [],
}
})]
} as ComponentMeta<typeof DraftsContainer>;

View File

@@ -10,7 +10,7 @@ import { NotificationsService } from 'src/services';
import { getDateDifference } from 'src/utils/helperFunctions';
import { useAppDispatch } from 'src/utils/hooks';
import { useReduxEffect } from 'src/utils/hooks/useReduxEffect';
import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage';
import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage';
interface Props {
id?: string;
@@ -28,7 +28,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) {
const [deleteStory] = useDeleteStoryMutation({
refetchQueries: ['GetMyDrafts']
})
const { setValue } = useFormContext<IStoryFormInputs>()
const { setValue } = useFormContext<CreateStoryType>()
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(false)
@@ -45,7 +45,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) {
setValue('title', data.getPostById.title);
setValue('tags', data.getPostById.tags);
setValue('body', data.getPostById.body);
setValue('cover_image', data.getPostById.cover_image ? [data.getPostById.cover_image] : []);
setValue('cover_image', data.getPostById.cover_image ? { url: data.getPostById.cover_image, id: null, name: null } : null);
setValue('is_published', data.getPostById.is_published);
}

View File

@@ -1,8 +1,8 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form";
import Button from "src/Components/Button/Button";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import { Tag } from "src/graphql";
import * as yup from "yup";
import ContentEditor from "../ContentEditor/ContentEditor";
@@ -29,7 +29,7 @@ const schema = yup.object({
interface IFormInputs {
title: string
tags: NestedValue<object[]>
tags: NestedValue<Tag[]>
cover_image: NestedValue<File[]> | string
body: string
}
@@ -60,7 +60,7 @@ export default function QuestionForm() {
<div
className='bg-white shadow-lg rounded-8 overflow-hidden'>
<div className="p-32">
<Controller
{/* <Controller
control={control}
name="cover_image"
render={({ field: { onChange, value, onBlur } }) => (
@@ -71,7 +71,7 @@ export default function QuestionForm() {
uploadText='Add a cover image'
/>
)}
/>
/> */}
<p className='input-error'>{errors.cover_image?.message}</p>
@@ -95,9 +95,18 @@ export default function QuestionForm() {
<p className="text-body5 mt-16">
Tags
</p>
<TagsInput
placeholder="Enter your tag and click enter. You can add multiple tags to your post"
classes={{ container: 'mt-8' }}
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput
placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
)}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}

View File

@@ -13,7 +13,6 @@ export default {
decorators: [WithModals, WrapForm<IStoryFormInputs>({
defaultValues: {
tags: [],
cover_image: [],
}
})]
} as ComponentMeta<typeof StoryForm>;

View File

@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { Controller, useFormContext } from "react-hook-form";
import Button from "src/Components/Button/Button";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import ContentEditor from "../ContentEditor/ContentEditor";
import { useCreateStoryMutation } from 'src/graphql'
@@ -13,7 +12,8 @@ import { createRoute } from 'src/utils/routing';
import PreviewPostCard from '../PreviewPostCard/PreviewPostCard'
import { StorageService } from 'src/services';
import { useThrottledCallback } from '@react-hookz/web';
import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage';
import { CreateStoryType } from '../../CreateStoryPage/CreateStoryPage';
import CoverImageInput from 'src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput';
interface Props {
isUpdating?: boolean;
@@ -29,7 +29,7 @@ export default function StoryForm(props: Props) {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext<IStoryFormInputs>();
const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext<CreateStoryType>();
const [editMode, setEditMode] = useState(true)
@@ -80,7 +80,7 @@ export default function StoryForm(props: Props) {
refetchQueries: ['GetMyDrafts']
});
const clickSubmit = (publish_now: boolean) => handleSubmit<IStoryFormInputs>(data => {
const clickSubmit = (publish_now: boolean) => handleSubmit<CreateStoryType>(data => {
setLoading(true);
createStory({
variables: {
@@ -90,7 +90,7 @@ export default function StoryForm(props: Props) {
body: data.body,
tags: data.tags.map(t => t.title),
is_published: publish_now,
cover_image: (data.cover_image[0] ?? null) as string | null,
cover_image: data.cover_image,
},
}
})
@@ -103,6 +103,8 @@ export default function StoryForm(props: Props) {
const { ref: registerTitleRef, ...titleRegisteration } = register('title');
return (
<>
<div id='preview-switch' className="flex gap-16">
@@ -117,19 +119,21 @@ export default function StoryForm(props: Props) {
<div
className='bg-white border-2 border-gray-200 rounded-16 overflow-hidden'>
<div className="p-16 md:p-24 lg:p-32">
<Controller
control={control}
name="cover_image"
render={({ field: { onChange, value, onBlur, ref } }) => (
<FilesInput
ref={ref}
<div className="w-full h-[120px] md:h-[240px] rounded-12 mb-16 overflow-hidden">
<Controller
control={control}
name="cover_image"
render={({ field: { onChange, value, onBlur, ref } }) => <CoverImageInput
value={value}
onBlur={onBlur}
onChange={onChange}
uploadText='Add a cover image'
onChange={e => {
onChange(e)
}}
// uploadText='Add a cover image'
/>
)}
/>
}
/>
</div>
@@ -153,11 +157,21 @@ export default function StoryForm(props: Props) {
/>
</div>
<TagsInput
placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }}
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput
placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
)}
/>
</div>
<ContentEditor
key={postId}
@@ -167,7 +181,7 @@ export default function StoryForm(props: Props) {
/>
</div>
</>}
{!editMode && <PreviewPostCard post={{ ...getValues(), cover_image: getValues().cover_image[0] }} />}
{!editMode && <PreviewPostCard post={{ ...getValues(), cover_image: getValues('cover_image.url') }} />}
<div className="flex gap-16 mt-32">
<Button
type='submit'

View File

@@ -4,54 +4,42 @@ import { useRef, useState } from "react";
import { ErrorBoundary, withErrorBoundary } from "react-error-boundary";
import { FormProvider, NestedValue, Resolver, useForm } from "react-hook-form";
import ErrorPage from "src/Components/Errors/ErrorPage/ErrorPage";
import { Post_Type } from "src/graphql";
import { CreateStoryMutationVariables, Post_Type } from "src/graphql";
import { StorageService } from "src/services";
import { useAppSelector } from "src/utils/hooks";
import { Override } from "src/utils/interfaces";
import { imageSchema, tagSchema } from "src/utils/validation";
import * as yup from "yup";
import DraftsContainer from "../Components/DraftsContainer/DraftsContainer";
import ErrorsContainer from "../Components/ErrorsContainer/ErrorsContainer";
import StoryForm from "../Components/StoryForm/StoryForm";
import styles from './styles.module.scss'
const FileSchema = yup.lazy((value: string | File[]) => {
switch (typeof value) {
case 'object':
return yup.mixed()
.test("fileSize", "File Size is too large", file => file.size <= 5242880)
.test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed",
(file: File) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type))
case 'string':
return yup.string().url();
default:
return yup.mixed()
}
})
const schema = yup.object({
title: yup.string().trim().required().min(10, 'Story title must be 2+ words').transform(v => v.replace(/(\r\n|\n|\r)/gm, "")),
tags: yup.array().required().min(1, 'Add at least one tag'),
body: yup.string().required().min(50, 'Post must contain at least 10+ words'),
cover_image: yup.array().of(FileSchema as any)
tags: yup.array().of(tagSchema).required().min(1, 'Add at least one tag'),
body: yup.string().required("Write some content in the post").min(50, 'Post must contain at least 10+ words'),
cover_image: imageSchema.nullable(true),
}).required();
export interface IStoryFormInputs {
id: number | null
title: string
tags: NestedValue<{ title: string }[]>
cover_image: NestedValue<File[]> | NestedValue<string[]>
body: string
is_published: boolean | null
type ApiStoryInput = NonNullable<CreateStoryMutationVariables['data']>;
export type IStoryFormInputs = {
id: ApiStoryInput['id']
title: ApiStoryInput['title']
body: ApiStoryInput['body']
cover_image: NestedValue<NonNullable<ApiStoryInput['cover_image']>> | null
tags: NestedValue<ApiStoryInput['tags']>
is_published: ApiStoryInput['is_published']
}
export type CreateStoryType = Override<IStoryFormInputs, {
cover_image: ApiStoryInput['cover_image'],
tags: { title: string }[]
cover_image: File[] | string[]
}>
const storageService = new StorageService<CreateStoryType>('story-edit');
@@ -64,13 +52,13 @@ function CreateStoryPage() {
story: state.staging.story || storageService.get()
}))
const formMethods = useForm<IStoryFormInputs>({
resolver: yupResolver(schema) as Resolver<IStoryFormInputs>,
const formMethods = useForm<CreateStoryType>({
resolver: yupResolver(schema) as Resolver<CreateStoryType>,
shouldFocusError: false,
defaultValues: {
id: story?.id ?? null,
title: story?.title ?? '',
cover_image: story?.cover_image ?? [],
cover_image: story?.cover_image,
tags: story?.tags ?? [],
body: story?.body ?? '',
is_published: story?.is_published ?? false,

View File

@@ -31,7 +31,8 @@ export default function StoryPageContent({ story }: Props) {
<Card id="content" onlyMd className="relative max">
{story.cover_image &&
<img src={story.cover_image}
className='w-full object-cover rounded-12 md:rounded-16 mb-16'
className='w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16'
// className='w-full object-cover rounded-12 md:rounded-16 mb-16'
alt="" />}
<div className="flex flex-col gap-24 relative">
{curUser?.id === story.author.id && <Menu

View File

@@ -28,7 +28,7 @@ export const useUpdateStory = (story: Story) => {
const handleEdit = () => {
dispatch(stageStory({
...story,
cover_image: story.cover_image ? [story.cover_image] : []
cover_image: story.cover_image ? { id: null, name: null, url: story.cover_image } : null,
}))
navigate("/blog/create-post?type=story")

View File

@@ -1,9 +1,8 @@
import { SubmitHandler, useForm } from "react-hook-form"
import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables, UserBasicInfoFragmentDoc } from "src/graphql";
import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { useAppDispatch, usePrompt } from "src/utils/hooks";
import SaveChangesCard from "../SaveChangesCard/SaveChangesCard";
import { toast } from "react-toastify";
@@ -12,15 +11,18 @@ import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
import { setUser } from "src/redux/features/user.slice";
import UpdateProfileAboutTabSkeleton from "./BasicProfileInfoTab.Skeleton";
import { useApolloClient } from "@apollo/client";
import AvatarInput from "src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput";
import { imageSchema } from "src/utils/validation";
interface Props {
}
type IFormInputs = NonNullable<UpdateProfileAboutMutationVariables['data']>;
const schema: yup.SchemaOf<IFormInputs> = yup.object({
name: yup.string().trim().required().min(2),
avatar: yup.string().url().required(),
avatar: imageSchema.required(),
bio: yup.string().ensure(),
email: yup.string().email().ensure(),
github: yup.string().ensure(),
@@ -55,8 +57,10 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
export default function BasicProfileInfoTab() {
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({
defaultValues: {},
const { register, formState: { errors, isDirty, }, handleSubmit, reset, control } = useForm<IFormInputs>({
defaultValues: {
},
resolver: yupResolver(schema),
mode: 'onBlur',
});
@@ -65,7 +69,7 @@ export default function BasicProfileInfoTab() {
const profileQuery = useMyProfileAboutQuery({
onCompleted: data => {
if (data.me)
reset(data.me)
reset({ ...data.me, avatar: { url: data.me.avatar } })
}
})
const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
@@ -107,7 +111,7 @@ export default function BasicProfileInfoTab() {
onCompleted: ({ updateProfileDetails: data }) => {
if (data) {
dispatch(setUser(data))
reset(data);
reset({ ...data, avatar: { url: data.avatar } });
apolloClient.writeFragment({
id: `User:${data?.id}`,
data,
@@ -123,12 +127,21 @@ export default function BasicProfileInfoTab() {
})
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<Card className="md:col-span-2" defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={profileQuery.data.me.avatar} width={120} />
<Controller
control={control}
name="avatar"
render={({ field: { onChange, value } }) => (
<AvatarInput value={value} onChange={onChange} width={120} />
)}
/>
</div>
</div>
<div className="p-16 md:p-24 mt-64">
@@ -148,29 +161,14 @@ export default function BasicProfileInfoTab() {
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Bio
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
rows={4}
className="input-text"
placeholder='Tell others a little bit about yourself'
{...register("bio")}
/>

View File

@@ -2,23 +2,16 @@ import { motion } from 'framer-motion'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { useEffect, useState } from "react"
import { Grid } from "react-loader-spinner";
import { CONSTS } from "src/utils";
import { QRCodeSVG } from 'qrcode.react';
import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
import { useApolloClient } from '@apollo/client';
import { IoClose } from 'react-icons/io5';
import { fetchLnurlAuth } from 'src/api/auth';
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url?action=link', {
credentials: 'include'
})
const data = await res.json()
return data;
}
const useLnurlQuery = () => {
const [loading, setLoading] = useState(true)