feat: add description to tags, update the tags input structure, make post title biggest

This commit is contained in:
MTG2000
2022-07-11 14:52:01 +03:00
parent 6a55cb5b04
commit 3f6c344d16
15 changed files with 101 additions and 53 deletions

View File

@@ -173,6 +173,7 @@ export interface NexusGenObjects {
votes_count: number; // Int!
}
Tag: { // root type
description?: string | null; // String
icon?: string | null; // String
id: number; // Int!
isOfficial?: boolean | null; // Boolean
@@ -374,6 +375,7 @@ export interface NexusGenFieldTypes {
votes_count: number; // Int!
}
Tag: { // field return type
description: string | null; // String
icon: string | null; // String
id: number; // Int!
isOfficial: boolean | null; // Boolean
@@ -571,6 +573,7 @@ export interface NexusGenFieldTypeNames {
votes_count: 'Int'
}
Tag: { // field return type name
description: 'String'
icon: 'String'
id: 'Int'
isOfficial: 'Boolean'

View File

@@ -201,6 +201,7 @@ input StoryInputType {
}
type Tag {
description: String
icon: String
id: Int!
isOfficial: Boolean

View File

@@ -7,6 +7,7 @@ const Tag = objectType({
t.nonNull.int('id');
t.nonNull.string('title');
t.string('icon');
t.string('description');
t.boolean('isOfficial');
}
});

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Tag" ADD COLUMN "description" TEXT;

View File

@@ -12,10 +12,11 @@ generator client {
// -----------------
model Tag {
id Int @id @default(autoincrement())
title String @unique
icon String?
isOfficial Boolean @default(false)
id Int @id @default(autoincrement())
title String @unique
icon String?
description String?
isOfficial Boolean @default(false)
project Project[]
stories Story[]

View File

@@ -4,18 +4,17 @@ import Badge from "src/Components/Badge/Badge";
// import CreatableSelect from 'react-select/creatable';
import Select from 'react-select'
import { OnChangeValue, StylesConfig, components, OptionProps } from "react-select";
import { useOfficialTagsQuery } from "src/graphql";
import { OfficialTagsQuery, useOfficialTagsQuery } from "src/graphql";
import React from "react";
interface Option {
readonly label: string;
readonly value: string;
readonly icon: string | null
readonly description: string | null
}
type Tag = {
title: string,
icon: string | null
}
type Tag = Omit<OfficialTagsQuery['officialTags'][number], 'id'>
interface Props {
classes?: {
@@ -29,32 +28,52 @@ interface Props {
const transformer = {
tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon }),
optionToTag: (o: Option): Tag => ({ title: o.value, icon: null })
tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon, description: tag.description }),
optionToTag: (o: Option): Tag => ({ title: o.value, icon: o.icon, description: o.description, })
}
const OptionComponent = (props: OptionProps<Option>) => {
return (
<div>
<components.Option {...props} className='flex items-start'>
<span className={`rounded-8 w-40 h-40 text-center py-8`}>
<components.Option {...props} className='!flex items-center gap-16 !py-16'>
<div className={`rounded-8 w-40 h-40 text-center py-8 shrink-0 bg-gray-100`}>
{props.data.icon}
</span>
<span className="self-center px-16">
{props.data.label}
</span>
</div>
<div>
<p className="font-medium self-center">
{props.data.label}
</p>
<p className="text-body5 text-gray-500">
{props.data.description}
</p>
</div>
</components.Option>
</div>
);
};
const { ValueContainer, Placeholder } = components;
const CustomValueContainer = ({ children, ...props }: any) => {
return (
<ValueContainer {...props}>
{React.Children.map(children, child =>
child && child.type !== Placeholder ? child : null
)}
<Placeholder {...props} isFocused={props.isFocused}>
{props.selectProps.placeholder}
</Placeholder>
</ValueContainer>
);
};
const colourStyles: StylesConfig = {
control: (styles, state) => ({
...styles,
padding: '1px 4px',
borderRadius: 8,
padding: '1px 0',
border: 'none'
}),
indicatorSeparator: (styles, state) => ({
...styles,
@@ -66,6 +85,12 @@ const colourStyles: StylesConfig = {
boxShadow: 'none !important'
},
}),
multiValue: styles => ({
...styles,
padding: '4px 12px',
borderRadius: 48,
fontWeight: 500
})
}
@@ -84,7 +109,7 @@ export default function TagsInput({
const handleChange = (newValue: OnChangeValue<Option, true>,) => {
onChange([...value, ...newValue.map(transformer.optionToTag)]);
onChange([...newValue.map(transformer.optionToTag)]);
onBlur();
}
@@ -95,9 +120,12 @@ export default function TagsInput({
}
const maxReached = value.length >= max;
const tagsOptions = (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption);
const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder;
const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption) : [];
return (
<div className={`${classes?.container}`}>
@@ -105,19 +133,23 @@ export default function TagsInput({
isLoading={officalTags.loading}
options={tagsOptions}
isMulti
isDisabled={maxReached}
placeholder={maxReached ? `Max. ${max} tags reached. Remove a tag to add another.` : placeholder}
isOptionDisabled={() => maxReached}
placeholder={currentPlaceholder}
isClearable
noOptionsMessage={() => {
return maxReached
? "You've reached the max number of tags."
: "No tags available";
}}
value={[]}
closeMenuOnSelect={false}
value={value.map(transformer.tagToOption)}
onChange={handleChange as any}
onBlur={onBlur}
components={{
Option: OptionComponent,
MultiValue: () => <></>
// ValueContainer: CustomValueContainer
}}
styles={colourStyles as any}
theme={(theme) => ({
...theme,
@@ -128,9 +160,9 @@ export default function TagsInput({
},
})}
/>
<div className="flex mt-16 gap-8 flex-wrap">
{/* <div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div>
</div> */}
</div>
)
}

View File

@@ -3,5 +3,6 @@ query OfficialTags {
id
title
icon
description
}
}

View File

@@ -143,15 +143,13 @@ export default function StoryForm() {
<p className='input-error'>{errors.cover_image?.message}</p>
<p className="text-body5 mt-16">
Title
</p>
<div className="input-wrapper mt-8 relative">
<div className="mt-16 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='Your Story Title'
className="p-0 text-[42px] border-0 focus:border-0 focus:outline-none focus:ring-0 font-bolder placeholder:!text-gray-600"
placeholder='Your Story Title...'
{...register("title")}
/>
</div>
@@ -159,12 +157,9 @@ export default function StoryForm() {
{errors.title.message}
</p>}
<p className="text-body5 mt-16">
Tags
</p>
<TagsInput
placeholder="Select up to 5 tags from the most popular ones"
classes={{ container: 'mt-8' }}
placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}

View File

@@ -20,7 +20,9 @@ export default function CreatePostPage() {
return (<>
<Helmet>
<title>Create Post</title>
{postType === 'story' && <title>Create Story</title>}
{postType === 'bounty' && <title>Create Bounty</title>}
{postType === 'question' && <title>Create Question</title>}
</Helmet>
<div
className="page-container grid gap-24 grid-cols-1 lg:grid-cols-[1fr_min(100%,910px)_1fr]"
@@ -40,9 +42,9 @@ export default function CreatePostPage() {
width: "min(100%,910px)"
}}>
{postType === 'story' && <>
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
{/* <h2 className="text-h2 font-bolder text-gray-800 mb-32">
Write a Story
</h2>
</h2> */}
<StoryForm />
</>}
{postType === 'bounty' && <>

View File

@@ -28,10 +28,10 @@ export default function AuthorCard({ author }: Props) {
</div>
<Button
fullWidth
href={`/profile/${author.id}`}
href={createRoute({ type: 'profile', id: author.id, username: author.name })}
color="primary"
className="mt-16">
Follow
Maker's Profile
</Button>
</div>
)

View File

@@ -49,7 +49,7 @@ export default function StoryPageContent({ story }: Props) {
Delete
</MenuItem>
</Menu>}
<h1 className="text-h2 font-bolder">{story.title}</h1>
<h1 className="text-[42px] font-bolder">{story.title}</h1>
<Header size="lg" showTimeAgo={false} author={story.author} date={story.createdAt} />
{story.tags.length > 0 && <div className="flex gap-8">
{story.tags.map(tag => <Badge key={tag.id} size='sm'>

View File

@@ -37,7 +37,7 @@ export default function PreviewPostContent({ post }: Props) {
alt="" />}
<div className="flex flex-col gap-24">
<Header size="lg" showTimeAgo={false} author={post.author} date={post.createdAt} />
<h1 className="text-h2 font-bolder">{post.title}</h1>
<h1 className="text-[42px] font-bolder">{post.title}</h1>
{post.tags.length > 0 && <div className="flex gap-8">
{post.tags.map((tag, idx) => <Badge key={idx} size='sm'>
{tag.title}

View File

@@ -340,6 +340,7 @@ export type StoryInputType = {
export type Tag = {
__typename?: 'Tag';
description: Maybe<Scalars['String']>;
icon: Maybe<Scalars['String']>;
id: Scalars['Int'];
isOfficial: Maybe<Scalars['Boolean']>;
@@ -401,7 +402,7 @@ export type Vote = {
export type OfficialTagsQueryVariables = Exact<{ [key: string]: never; }>;
export type OfficialTagsQuery = { __typename?: 'Query', officialTags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null }> };
export type OfficialTagsQuery = { __typename?: 'Query', officialTags: Array<{ __typename?: 'Tag', id: number, title: string, icon: string | null, description: string | null }> };
export type NavCategoriesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -557,6 +558,7 @@ export const OfficialTagsDocument = gql`
id
title
icon
description
}
}
`;

View File

@@ -5,27 +5,37 @@ export const tags = [
{
id: 1,
title: 'Bitcoin',
description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus',
icon: "🅱",
isOfficial: true,
},
{
id: 2,
title: 'Lightning',
description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus',
icon: "⚡",
isOfficial: true,
},
{
id: 3,
title: 'Webln',
description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus',
icon: "🔗",
isOfficial: true,
},
{
id: 4,
title: 'Gaming',
description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus',
icon: "🎮",
isOfficial: true,
},
{
id: 5,
title: 'Design',
icon: '🎨'
description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus',
icon: '🎨',
isOfficial: true,
}
].map(i => ({ __typename: "Tag", ...i })) as Tag[]

View File

@@ -1,8 +1,6 @@
import { Tag as ApiTag } from "src/graphql";
export type Tag = {
id: number
title: string
}
export type Tag = ApiTag;
export type ListComponentProps<T> = {