mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-31 12:14:30 +01:00
feat: add description to tags, update the tags input structure, make post title biggest
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -201,6 +201,7 @@ input StoryInputType {
|
||||
}
|
||||
|
||||
type Tag {
|
||||
description: String
|
||||
icon: String
|
||||
id: Int!
|
||||
isOfficial: Boolean
|
||||
|
||||
@@ -7,6 +7,7 @@ const Tag = objectType({
|
||||
t.nonNull.int('id');
|
||||
t.nonNull.string('title');
|
||||
t.string('icon');
|
||||
t.string('description');
|
||||
t.boolean('isOfficial');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Tag" ADD COLUMN "description" TEXT;
|
||||
@@ -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[]
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ query OfficialTags {
|
||||
id
|
||||
title
|
||||
icon
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' && <>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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[]
|
||||
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user