diff --git a/backend/ormconfig.ts b/backend/ormconfig.ts index 860837bcc6705c050b3eea3df2989b9199d70a8d..03c8c57320d33f0b9c52480ba0ca4823230151ef 100644 --- a/backend/ormconfig.ts +++ b/backend/ormconfig.ts @@ -1,5 +1,4 @@ import { Attachment } from "src/entities/attachment.entity"; -import { AttachmentType } from "src/entities/attachmenttype.entity"; import { Comment } from "src/entities/comment.entity"; import { Delegator } from "src/entities/delegator.entity"; import { Drep } from "src/entities/drep.entity"; @@ -15,7 +14,7 @@ const configVoltaire: PostgresConnectionOptions = { port: 5432, username: "postgres", password: "postgres", - entities: [Drep, Note,Attachment, AttachmentType, Delegator, Comment, Reaction], + entities: [Drep, Note,Attachment, Delegator, Comment, Reaction], //Setting to true will update in real time for dev envt only. In prod, risks loss of data synchronize: true, }; diff --git a/backend/src/dto/createNoteDto.ts b/backend/src/dto/createNoteDto.ts index a486c7a1158bdba92448bb179bda5c007af3275e..54f1ae35b94deab1946d5ccbaa40d344ce18e40b 100644 --- a/backend/src/dto/createNoteDto.ts +++ b/backend/src/dto/createNoteDto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty } from 'class-validator'; +import { IsNotEmpty, IsOptional } from 'class-validator'; export class createNoteDto { @IsNotEmpty() @@ -13,4 +13,6 @@ export class createNoteDto { voter: string; @IsNotEmpty() note_visibility: string; + @IsOptional() + attachments: string[]; } diff --git a/backend/src/entities/attachment.entity.ts b/backend/src/entities/attachment.entity.ts index 27b38c26fac89fbaf75f0127f1f28fd01af694ef..e1fb1c61566ef45ced6a5039c448e89fe7381164 100644 --- a/backend/src/entities/attachment.entity.ts +++ b/backend/src/entities/attachment.entity.ts @@ -7,8 +7,17 @@ import { PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { AttachmentType } from './attachmenttype.entity'; import { Note } from './note.entity'; +enum AttachmentTypeName { + Link = 'link', + PDF = 'pdf', + JPG = 'jpg', + PNG = 'png', + WEBP = 'webp', + GIF = 'gif', + SVG = 'svg', +} + @Entity() export class Attachment { @@ -19,14 +28,15 @@ export class Attachment { @Column({ unique: true, nullable: false }) url: string; - @ManyToMany(() => Note, (note) => note.attachments) - notes: Note[]; + @ManyToOne(() => Note, (note) => note.id) + noteId: Note[]; - @ManyToOne( - () => AttachmentType, - (attachmentType) => attachmentType.attachments, - ) - attachmentType: AttachmentType; + @Column({ + type: 'enum', + enum: AttachmentTypeName, + default: AttachmentTypeName.Link, // Set default value if needed + }) + attachmentType: AttachmentTypeName; @CreateDateColumn() createdAt: Date; diff --git a/backend/src/entities/attachmenttype.entity.ts b/backend/src/entities/attachmenttype.entity.ts deleted file mode 100644 index 110d885f8ff4a3df8b8d20c116600b276c468146..0000000000000000000000000000000000000000 --- a/backend/src/entities/attachmenttype.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; -import { Attachment } from './attachment.entity'; - -enum AttachmentTypeName { - Link = 'link', - PDF = 'pdf', - JPG = 'jpg', - PNG = 'png', - WEBP = 'webp', - GIF = 'gif', - SVG = 'svg', -} - -@Entity() -export class AttachmentType { - @PrimaryGeneratedColumn() - id: number; - - @Column({ - type: 'enum', - enum: AttachmentTypeName, - default: AttachmentTypeName.Link, // Set default value if needed - }) - name: AttachmentTypeName; - - @OneToMany(() => Attachment, (attachment) => attachment.attachmentType) - attachments: Attachment[]; -} diff --git a/backend/src/entities/comment.entity.ts b/backend/src/entities/comment.entity.ts index a5ee35076e7c48eb27f957b21119071cbed3d904..ba4cf45d606d0e73c8c54c6e0e3cffceae20c9ce 100644 --- a/backend/src/entities/comment.entity.ts +++ b/backend/src/entities/comment.entity.ts @@ -10,7 +10,10 @@ import { } from 'typeorm'; import { Note } from './note.entity'; import { Delegator } from './delegator.entity'; - +enum ParentEntityType { + Note = 'note', + Comment = 'comment', +} @Entity() export class Comment { @PrimaryGeneratedColumn() @@ -19,14 +22,24 @@ export class Comment { @Column() content: string; - @ManyToOne(() => Note, (note) => note.comments) + @Column({ + type: 'enum', + enum: ParentEntityType, + default: ParentEntityType.Note, // Set default value if needed + }) + parentEntity: ParentEntityType; + + @Column({ type: 'int', nullable: false }) + parentId: number; + + @ManyToOne(() => Note, (note) => note.id) // Many-to-One relationship with Note note: Note; - @ManyToOne(() => Delegator, (delegator) => delegator.comments) // Many-to-One relationship with Delegator - delegator: Delegator; + @ManyToOne(() => Comment, (comment) => comment.id) // Many-to-One relationship with Comment + comment: Comment; - @ManyToMany(() => Delegator) - reactions: Delegator[]; + @ManyToOne(() => Delegator, (delegator) => delegator.id) // Many-to-One relationship with Delegator + delegator: Delegator; @CreateDateColumn() createdAt: Date; diff --git a/backend/src/entities/delegator.entity.ts b/backend/src/entities/delegator.entity.ts index 851ffbc68508b7cc4e4d271c892872c4f850a39c..43e8f1c1257b9237e90bfe8d15787f43ed8b2dad 100644 --- a/backend/src/entities/delegator.entity.ts +++ b/backend/src/entities/delegator.entity.ts @@ -4,33 +4,19 @@ import { Column, CreateDateColumn, UpdateDateColumn, - OneToMany, - ManyToMany, } from 'typeorm'; -import { Comment } from './comment.entity'; -import { Reaction } from './reaction.entity'; -import { Note } from './note.entity'; @Entity() export class Delegator { - //delegator - //auto increment primary key decorator @PrimaryGeneratedColumn() id: number; - //Human readable name for the entity + @Column({ unique: true, nullable: false }) name: string; - @Column({ nullable: false }) - wallet_addr: string; - //a delegator can have many comments - @OneToMany(() => Comment, (comment) => comment.delegator) - comments: Comment[]; - //a delegator can have many rxns but of different types - @OneToMany(() => Reaction, (reaction) => reaction.delegator) - reactions: Reaction[]; + @Column({ nullable: true }) + voter_id: string; - //timestamps @CreateDateColumn() createdAt: Date; diff --git a/backend/src/entities/drep.entity.ts b/backend/src/entities/drep.entity.ts index f04a259aa6ea5edac3d3e0ef4c5ef45e66a621e3..72b8eaf1b6584f660b94ed58403f8f1b1a8a340b 100644 --- a/backend/src/entities/drep.entity.ts +++ b/backend/src/entities/drep.entity.ts @@ -34,9 +34,6 @@ export class Drep { @Column({ nullable: false, unique: true }) voter_id: string; - @OneToMany(() => Note, (note) => note.voter) - notes: Note[]; - @CreateDateColumn() createdAt: Date; diff --git a/backend/src/entities/note.entity.ts b/backend/src/entities/note.entity.ts index a9c9ad6be67d8a4499e93c92aa0e780b4a637a85..7fd03e133d1abb4087e09ee168855c3664e15787 100644 --- a/backend/src/entities/note.entity.ts +++ b/backend/src/entities/note.entity.ts @@ -10,9 +10,6 @@ import { DeleteDateColumn, } from 'typeorm'; import { Drep } from './drep.entity'; -import { Attachment } from './attachment.entity'; -import { Comment } from './comment.entity'; -import { Reaction } from './reaction.entity'; @Entity() export class Note { @@ -32,15 +29,6 @@ export class Note { @ManyToOne(() => Drep, (drep) => drep.voter_id) voter: Drep; - @OneToMany(() => Comment, (comment) => comment.note) - comments: Comment[]; - - @ManyToMany(() => Reaction, (reaction) => reaction.note) - reactions: string; - - @ManyToMany(() => Attachment, (attachment) => attachment.notes) - attachments: Attachment[]; - @Column() note_visibility: string; diff --git a/backend/src/entities/reaction.entity.ts b/backend/src/entities/reaction.entity.ts index 88bbc02494e3a76902a1adac1e0f5b2e8c1cbe9b..23f2afdcf4910654c92de69ab49c137620617679 100644 --- a/backend/src/entities/reaction.entity.ts +++ b/backend/src/entities/reaction.entity.ts @@ -10,6 +10,7 @@ import { } from 'typeorm'; import { Delegator } from './delegator.entity'; import { Note } from './note.entity'; +import { Comment } from './comment.entity'; enum ReactionTypeName { Like = 'like', @@ -17,6 +18,10 @@ enum ReactionTypeName { ThumbsDown = 'thumbsdown', Rocket = 'rocket', } +enum ParentEntityType { + Note = 'note', + Comment = 'comment', +} @Entity() @Unique(['delegator', 'type']) // Ensures delegator cant like or thumbs up twice @@ -33,10 +38,23 @@ export class Reaction { }) type: ReactionTypeName; - @ManyToMany(() => Note, (note) => note.reactions) + @Column({ + type: 'enum', + enum: ParentEntityType, + default: ParentEntityType.Note, // Set default value if needed + }) + parentEntity: ParentEntityType; + + @Column({ type: 'int', nullable: false }) + parentId: number; + + @ManyToOne(() => Comment, (comment) => comment.id) // Many-to-One relationship with Comment + comment: Comment; + + @ManyToMany(() => Note, (note) => note.id) note: Note[]; - @ManyToOne(() => Delegator, (delegator) => delegator.reactions) // Many-to-One relationship with Delegator + @ManyToOne(() => Delegator, (delegator) => delegator.id) // Many-to-One relationship with Delegator delegator: Delegator; //timestamps @CreateDateColumn() diff --git a/backend/src/note/note.service.ts b/backend/src/note/note.service.ts index 38116446b6b7951be4490dbd31fb0c2db9cae499..054a436afc2ddbefe458082ef1d09fee8f1e9cf6 100644 --- a/backend/src/note/note.service.ts +++ b/backend/src/note/note.service.ts @@ -4,11 +4,13 @@ import { createNoteDto } from 'src/dto'; import { DataSource, Repository } from 'typeorm'; import { ConnectionService } from 'src/connection/connection.service'; import { Note } from 'src/entities/note.entity'; +import { Attachment } from 'src/entities/attachment.entity'; @Injectable() export class NoteService { constructor( @InjectRepository(Note) private noteRepo: Repository<Note>, + @InjectRepository(Note) private attachmentRepo: Repository<Attachment>, private connectionService: ConnectionService, ) {} @@ -39,7 +41,6 @@ export class NoteService { const res = await queryInstance .getRepository('Note') .insert(modifiedNoteDto); - console.log(res.identifiers[0].id); return { noteAdded: res.identifiers[0].id }; } else { return new NotFoundException('DRep associated with note not found!'); diff --git a/frontend/public/note/link.svg b/frontend/public/note/link.svg index a9e996f753c22ac7bb6cb404ec30c59c028b03f0..b619f009303658bdd68807a710c82c07c5372fd4 100644 --- a/frontend/public/note/link.svg +++ b/frontend/public/note/link.svg @@ -1,3 +1,3 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.5 12.5004L12.5 7.50042M9.16667 5.00042L9.5525 4.55375C10.334 3.77236 11.3939 3.33342 12.499 3.3335C13.6042 3.33357 14.664 3.77266 15.4454 4.55417C16.2268 5.33567 16.6658 6.39558 16.6657 7.50071C16.6656 8.60585 16.2265 9.66569 15.445 10.4471L15 10.8338M10.8334 15.0004L10.5025 15.4454C9.71189 16.2272 8.64484 16.6657 7.53294 16.6657C6.42103 16.6657 5.35398 16.2272 4.56335 15.4454C4.17365 15.0601 3.86426 14.6013 3.65311 14.0955C3.44196 13.5898 3.33323 13.0472 3.33323 12.4992C3.33323 11.9511 3.44196 11.4085 3.65311 10.9028C3.86426 10.3971 4.17365 9.93825 4.56335 9.55292L5.00002 9.16708" stroke="#809CDE" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.49978 12.5004L12.4998 7.50042M9.16644 5.00042L9.55228 4.55375C10.3338 3.77236 11.3937 3.33342 12.4988 3.3335C13.604 3.33357 14.6638 3.77266 15.4452 4.55417C16.2266 5.33567 16.6655 6.39558 16.6654 7.50071C16.6654 8.60585 16.2263 9.66569 15.4448 10.4471L14.9998 10.8338M10.8331 15.0004L10.5023 15.4454C9.71167 16.2272 8.64462 16.6657 7.53271 16.6657C6.4208 16.6657 5.35375 16.2272 4.56313 15.4454C4.17342 15.0601 3.86404 14.6013 3.65289 14.0955C3.44173 13.5898 3.33301 13.0472 3.33301 12.4992C3.33301 11.9511 3.44173 11.4085 3.65289 10.9028C3.86404 10.3971 4.17342 9.93825 4.56313 9.55292L4.99979 9.16708" stroke="#0033AD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/public/note/photo.svg b/frontend/public/note/photo.svg index 5ff3171e7610c3931bc3eac421ea5aaed6782309..b9579c6f6a8a2260397b9a23bfeb4a98dff86282 100644 --- a/frontend/public/note/photo.svg +++ b/frontend/public/note/photo.svg @@ -1,3 +1,3 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.5 6.66667H12.5083M2.5 13.3333L6.66667 9.16666C7.44 8.4225 8.39333 8.4225 9.16667 9.16666L13.3333 13.3333M11.6667 11.6667L12.5 10.8333C13.2733 10.0892 14.2267 10.0892 15 10.8333L17.5 13.3333M2.5 5C2.5 4.33696 2.76339 3.70107 3.23223 3.23223C3.70107 2.76339 4.33696 2.5 5 2.5H15C15.663 2.5 16.2989 2.76339 16.7678 3.23223C17.2366 3.70107 17.5 4.33696 17.5 5V15C17.5 15.663 17.2366 16.2989 16.7678 16.7678C16.2989 17.2366 15.663 17.5 15 17.5H5C4.33696 17.5 3.70107 17.2366 3.23223 16.7678C2.76339 16.2989 2.5 15.663 2.5 15V5Z" stroke="#809CDE" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.5 6.66667H12.5083M2.5 13.3333L6.66667 9.16666C7.44 8.4225 8.39333 8.4225 9.16667 9.16666L13.3333 13.3333M11.6667 11.6667L12.5 10.8333C13.2733 10.0892 14.2267 10.0892 15 10.8333L17.5 13.3333M2.5 5C2.5 4.33696 2.76339 3.70107 3.23223 3.23223C3.70107 2.76339 4.33696 2.5 5 2.5H15C15.663 2.5 16.2989 2.76339 16.7678 3.23223C17.2366 3.70107 17.5 4.33696 17.5 5V15C17.5 15.663 17.2366 16.2989 16.7678 16.7678C16.2989 17.2366 15.663 17.5 15 17.5H5C4.33696 17.5 3.70107 17.2366 3.23223 16.7678C2.76339 16.2989 2.5 15.663 2.5 15V5Z" stroke="#0033AD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/src/app/[locale]/page.tsx b/frontend/src/app/[locale]/page.tsx index 42ea06cc9b82e3a1bef8a4b49aa616b6a0e688bb..f20dc8f60d2ba43cb387e14a146e04886b9bb8ca 100644 --- a/frontend/src/app/[locale]/page.tsx +++ b/frontend/src/app/[locale]/page.tsx @@ -34,7 +34,7 @@ const Page = () => { ) ).json(); setRaw(cip); - setComments(comments) + setComments(comments); } catch (error) { console.error('Error fetching Markdown:', error); } @@ -50,7 +50,7 @@ const Page = () => { <CIPIntro /> <CIPInfo /> <CIPMotivationInfo /> - <ConversationsCard conversations={comments}/> + <ConversationsCard conversations={comments} /> <CIPSpecifications /> <CIPDRepInfo /> <CIPGovernanceActions /> @@ -58,8 +58,8 @@ const Page = () => { <CIPChangelog /> <CIPPathtoactive /> <CIPAcknowledgments /> - <CopyRight/> - <Footer/> + <CopyRight /> + <Footer /> </Background> </div> ); diff --git a/frontend/src/assets/styles/globals.css b/frontend/src/assets/styles/globals.css index b1c2897fe760c22bf382744be4668afdc18a4a62..e552177f300996da1446a7dfc21385822a5ade39 100644 --- a/frontend/src/assets/styles/globals.css +++ b/frontend/src/assets/styles/globals.css @@ -156,22 +156,6 @@ body { } } } - -#overlay { - display: flex; - flex-direction: column; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 500px; - padding: 20px; - background: #fff; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); - z-index: 999; -} - #acknowledgements details { @apply border-b-2 border-b-gray-300 border-opacity-40 bg-white p-5; } diff --git a/frontend/src/components/1694.io/1694DRepInfo.tsx b/frontend/src/components/1694.io/1694DRepInfo.tsx index 534d8b4601aabfdac6814f062cd4b71025bf760f..e31f6ff909f9fc4a1c1de6157c22a151542e3845 100644 --- a/frontend/src/components/1694.io/1694DRepInfo.tsx +++ b/frontend/src/components/1694.io/1694DRepInfo.tsx @@ -5,8 +5,11 @@ import Separator from './Separator'; const CIPDRepInfo = () => { return ( <div className="bg-blue-800"> - <div className="max-w-3xl mx-auto py-10"> - <div id='delegated-representatives-dreps' className="mb-5 text-6xl font-bold text-violet-50"> + <div className="mx-auto max-w-3xl py-10"> + <div + id="delegated-representatives-dreps" + className="mb-5 text-6xl font-bold text-violet-50" + > <p>Delegated</p> <p>Representatives (DReps)</p> </div> @@ -15,7 +18,10 @@ const CIPDRepInfo = () => { type="warning" text="CIP-1694 DReps should not be conflated with Project Catalyst DReps." /> - <section id='pre-defined-dreps' className="flex flex-col gap-3 text-white"> + <section + id="pre-defined-dreps" + className="flex flex-col gap-3 text-white" + > <div className="flex w-full items-center justify-center"> <img src="/img/becomeDrepImg.png" alt="" width={'25%'} /> </div> @@ -49,7 +55,10 @@ const CIPDRepInfo = () => { /> </section> <Separator /> - <section id='registered-dreps' className="flex flex-col gap-3 text-white"> + <section + id="registered-dreps" + className="flex flex-col gap-3 text-white" + > <p className="text-3xl font-bold">Registered DReps </p> <p> In Voltaire, existing stake credentials will be able to delegate @@ -87,7 +96,10 @@ const CIPDRepInfo = () => { /> </section> <Separator /> - <section id='new-stake-distribution-for-dreps' className="flex flex-col gap-3 text-white"> + <section + id="new-stake-distribution-for-dreps" + className="flex flex-col gap-3 text-white" + > <p className="text-3xl font-bold">New stake distribution</p> <p> In addition to the existing per-stake-credential distribution and @@ -103,7 +115,7 @@ const CIPDRepInfo = () => { /> </section> <Separator /> - <section className="flex flex-col gap-3 text-white"> + <section className="flex flex-col gap-3 text-white"> <p className="text-3xl font-bold"> Incentives for Ada holders to delegate voting stake </p> @@ -122,7 +134,10 @@ const CIPDRepInfo = () => { /> </section> <Separator /> - <section id='drep-incentives' className="flex flex-col gap-3 text-white"> + <section + id="drep-incentives" + className="flex flex-col gap-3 text-white" + > <p className="text-3xl font-bold">DRep incentives</p> <p> DReps arguably need to be compensated for their work. Research on diff --git a/frontend/src/components/1694.io/1694GovernanceActions.tsx b/frontend/src/components/1694.io/1694GovernanceActions.tsx index 4f66275f1415b2434864bb054d64249dce4ea314..6253f14ba912121e9b4e18c562c40d71d3fd4791 100644 --- a/frontend/src/components/1694.io/1694GovernanceActions.tsx +++ b/frontend/src/components/1694.io/1694GovernanceActions.tsx @@ -10,7 +10,7 @@ const CIPGovernanceActions = () => { return ( <div className="bg-gradient-to-b from-[#E9EFFF] to-[#FFFFFF]"> <div className="container flex w-full flex-col items-center justify-center py-10"> - <div id='governance-actions' className="w-full"> + <div id="governance-actions" className="w-full"> <p className="text-start text-5xl font-bold text-zinc-800"> Governance actions </p> @@ -68,7 +68,7 @@ const CIPGovernanceActions = () => { /> </section> <Separator /> - <section id='ratification' className="my-5 flex flex-col gap-6"> + <section id="ratification" className="my-5 flex flex-col gap-6"> <p className="text-xl font-bold">Ratification</p> <p> Governance actions are ratified through on-chain voting actions. @@ -112,7 +112,9 @@ const CIPGovernanceActions = () => { enacting a hard-fork do not have unintended consequences in combination with other actions. </p> - <p id='requirements' className="text-lg font-extrabold">Requirements</p> + <p id="requirements" className="text-lg font-extrabold"> + Requirements + </p> <p> The following table details the ratification requirements for each governance action scenario. The columns represent: @@ -180,7 +182,9 @@ const CIPGovernanceActions = () => { type="info" text="To achieve legitimacy, the minimum acceptable threshold should be no less than 50% of the delegated stake." /> - <p id='restrictions' className="text-2xl font-bold text-zinc-800">Restrictions</p> + <p id="restrictions" className="text-2xl font-bold text-zinc-800"> + Restrictions + </p> <p> Apart from Treasury withdrawals and Infos, we include a mechanism for ensuring that governance actions of the same type do not @@ -192,7 +196,9 @@ const CIPGovernanceActions = () => { that two actions of the same type can be enacted at the same time, but they must be deliberately designed to do so. </p> - <p id='enactment' className="text-2xl font-bold text-zinc-800">Enactment</p> + <p id="enactment" className="text-2xl font-bold text-zinc-800"> + Enactment + </p> <p> Actions that have been ratified in the current epoch are prioritised as follows for enactment: @@ -235,7 +241,9 @@ const CIPGovernanceActions = () => { All governance actions are enacted on the epoch boundary after their ratification. </p> - <p id='lifecycle' className="text-2xl font-bold text-zinc-800">Lifecycle</p> + <p id="lifecycle" className="text-2xl font-bold text-zinc-800"> + Lifecycle + </p> <p>Every governance action will include the following:</p> <ul className="ml-8 flex list-decimal flex-col gap-2"> <li> @@ -270,7 +278,9 @@ const CIPGovernanceActions = () => { consisting of the transaction hash that created it and the index within the transaction body that points to it. </p> - <p id='protocol-parameter-groups' className="text-2xl font-bold">Protocol Parameter Groups</p> + <p id="protocol-parameter-groups" className="text-2xl font-bold"> + Protocol Parameter Groups + </p> <p> We have grouped the protocol parameter changes by type, allowing different thresholds to be set for each group. @@ -361,7 +371,9 @@ const CIPGovernanceActions = () => { members (ccMaxTermLength) </li> </ul> - <p id='votes' className="text-2xl font-bold">Votes</p> + <p id="votes" className="text-2xl font-bold"> + Votes + </p> <p>Each vote transaction consists of the following:</p> <ul className="ml-5 flex list-disc flex-col gap-2"> <li>a governance action ID</li> @@ -398,7 +410,9 @@ const CIPGovernanceActions = () => { governance action is ratified, voting ends and transactions containing further votes are invalid. </p> - <p id='governance-state' className="text-2xl font-bold">Governance State</p> + <p id="governance-state" className="text-2xl font-bold"> + Governance State + </p> <p> When a governance action is successfully submitted to the chain, its progress will be tracked by the ledger state. In particular, @@ -424,7 +438,12 @@ const CIPGovernanceActions = () => { the total 'Yes'/'No'/'Abstain' votes of the SPOs for this action </li> </ul> - <p id='changes-to-the-stake-snapshot' className="text-2xl font-bold">Changes to the stake snapshot</p> + <p + id="changes-to-the-stake-snapshot" + className="text-2xl font-bold" + > + Changes to the stake snapshot + </p> <p> Since the stake snapshot changes at each epoch boundary, a new tally must be calculated when each unratified governance action is @@ -432,7 +451,10 @@ const CIPGovernanceActions = () => { enacted even though the DRep or SPO votes have not changed (since the vote delegation could have changed). </p> - <p id='definitions-relating-to-voting-stake' className="text-2xl font-bold"> + <p + id="definitions-relating-to-voting-stake" + className="text-2xl font-bold" + > Definitions related to voting stake </p> <p>We define a number of new terms related to voting stake:</p> diff --git a/frontend/src/components/1694.io/1694Rationale.tsx b/frontend/src/components/1694.io/1694Rationale.tsx index 02fa94b03e13c09332693ed638b63344c0472706..7237386dc16370d9bbce1c0f72147e18bb24ca0b 100644 --- a/frontend/src/components/1694.io/1694Rationale.tsx +++ b/frontend/src/components/1694.io/1694Rationale.tsx @@ -3,7 +3,7 @@ import React from 'react'; const CIPRationale = () => { return ( <div className="bg-blue-800"> - <div className="max-w-3xl mx-auto flex w-full flex-col gap-5 py-10"> + <div className="mx-auto flex w-full max-w-3xl flex-col gap-5 py-10"> <div> <p className="text-start text-5xl font-bold text-white">Rationale</p> </div> diff --git a/frontend/src/components/1694.io/1694Specifications.tsx b/frontend/src/components/1694.io/1694Specifications.tsx index bbc45b957c3c792fa21add68e4f45b3f79cf6eab..2e6f0f3156524a8573125a062827b1b5f79d5738 100644 --- a/frontend/src/components/1694.io/1694Specifications.tsx +++ b/frontend/src/components/1694.io/1694Specifications.tsx @@ -15,7 +15,10 @@ const CIPSpecifications = () => { <div className="flex items-center justify-center"> <img src="/building.png" alt="Building" /> </div> - <div id='the-cardano-constitution' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="the-cardano-constitution" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold">The Cardano Constitution</p> <p> The Cardano Constitution is a text document that defines Cardano's @@ -32,7 +35,10 @@ const CIPSpecifications = () => { </p> </div> <Separator /> - <div id='the-constitutional-committee' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="the-constitutional-committee" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold">The constitutional committee</p> <p> We define a constitutional committee which represents a set of @@ -63,7 +69,10 @@ const CIPSpecifications = () => { </p> </div> <Separator /> - <div id='state-of-no-confidence' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="state-of-no-confidence" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold">State of no-confidence</p> <p> The constitutional committee is considered to be in one of the @@ -88,7 +97,10 @@ const CIPSpecifications = () => { </p> </div> <Separator /> - <div id='replacing-the-constitutional-committee' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="replacing-the-constitutional-committee" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold"> Replacing the constitutional committee </p> @@ -108,7 +120,10 @@ const CIPSpecifications = () => { </p> </div> <Separator /> - <div id='size-of-the-constitutional-committee' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="size-of-the-constitutional-committee" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold"> Size of the constitutional committee </p> @@ -132,7 +147,10 @@ const CIPSpecifications = () => { </p> </div> <Separator /> - <div id='term-limits' className="my-5 flex flex-col gap-5 font-extralight"> + <div + id="term-limits" + className="my-5 flex flex-col gap-5 font-extralight" + > <p className="text-2xl font-bold">Terms</p> <p> Each newly elected constitutional committee will have a term. diff --git a/frontend/src/components/1694.io/ConversationsCard.tsx b/frontend/src/components/1694.io/ConversationsCard.tsx index 345aa6c5258e47c1deb9f6f54d4187af4401fc91..20aa89776cf5ec61775542728c98a0c9faaee5f8 100644 --- a/frontend/src/components/1694.io/ConversationsCard.tsx +++ b/frontend/src/components/1694.io/ConversationsCard.tsx @@ -58,13 +58,15 @@ const ConversationsCard = ({ conversations }: ConversationsCardProps) => { </div> ))} </div> - <div className="flex flex-row items-center justify-center mt-5"> + <div className="mt-5 flex flex-row items-center justify-center"> <Button> {conversations ? ( <Link href={conversations[0].html_url}> Join The Conversation on Github </Link> - ): ("Loading conversations...")} + ) : ( + 'Loading conversations...' + )} </Button> </div> </div> diff --git a/frontend/src/components/1694.io/CopyRight.tsx b/frontend/src/components/1694.io/CopyRight.tsx index 575953d636dbbbb4694db93094caa3cd69f291c7..f9ae94d7957ace051c81e9a643b91f71ea63db11 100644 --- a/frontend/src/components/1694.io/CopyRight.tsx +++ b/frontend/src/components/1694.io/CopyRight.tsx @@ -7,8 +7,8 @@ const CopyRight = () => { <div className="container"> <p className="text-5xl font-bold text-zinc-800">Copyright</p> </div> - <HotLinks /> - <div className="max-w-3xl mx-auto"> + <HotLinks /> + <div className="mx-auto max-w-3xl"> <p> This CIP is licensed under{' '} <a diff --git a/frontend/src/components/1694.io/HotLinks.tsx b/frontend/src/components/1694.io/HotLinks.tsx index 991bcc1e3f5ed1a76a77190cb584c2695cb9b65e..f1ade45ff4b45452071324ad0a8db9a91c8fea95 100644 --- a/frontend/src/components/1694.io/HotLinks.tsx +++ b/frontend/src/components/1694.io/HotLinks.tsx @@ -99,7 +99,9 @@ const HotLinks = ({ seethrough }: HotLinksProps) => { }, ]; return ( - <div className={`my-5 bg-white ${seethrough ? 'bg-opacity-50' : ''} w-screen`}> + <div + className={`my-5 bg-white ${seethrough ? 'bg-opacity-50' : ''} w-screen`} + > <div className={`flex w-full flex-row flex-wrap items-center justify-center gap-3 p-8`} > @@ -110,7 +112,9 @@ const HotLinks = ({ seethrough }: HotLinksProps) => { key={item.title} className="flex w-fit items-center justify-center rounded-2xl bg-blue-100 px-2 py-1" > - <a href={item.link} className="text-nowrap text-sm text-black">{item.title}</a> + <a href={item.link} className="text-nowrap text-sm text-black"> + {item.title} + </a> </div> ))} </div> diff --git a/frontend/src/components/atoms/Header.tsx b/frontend/src/components/atoms/Header.tsx index dd310365b5be4bbdaee072fa89093b6ff97f99d0..2fdc58d16b361a297132eb3e7fab5c11726dd264 100644 --- a/frontend/src/components/atoms/Header.tsx +++ b/frontend/src/components/atoms/Header.tsx @@ -21,7 +21,7 @@ const Header = () => { return ( <header className="bg-white bg-opacity-50"> <div className="container flex flex-row items-center justify-between py-6"> - <Link href='/'> + <Link href="/"> <img src="/sancho1694.svg" alt="Sancho logo" width={'40%'} /> </Link> <div className="flex items-center gap-6 text-nowrap text-sm font-bold"> @@ -66,7 +66,7 @@ const Header = () => { </div> </div> </div> - <TranslationBlock /> + {activeLink === `/${currentLocale}` && <TranslationBlock />} </header> ); }; diff --git a/frontend/src/components/atoms/HoverLinkChip.tsx b/frontend/src/components/atoms/HoverLinkChip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..18267789749a9bf67e53e5aa71c0b8589319ff0a --- /dev/null +++ b/frontend/src/components/atoms/HoverLinkChip.tsx @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; + +interface HoverLinkChipProps { + children?: React.ReactNode; + link?: string; + handleClick?: () => void; + position?: 'top' | 'bottom'; +} +const HoverLinkChip = ({ + children, + link, + handleClick, + position = 'top', +}: HoverLinkChipProps) => { + const [isHovered, setIsHovered] = useState(false); + + return ( + <div + className="relative flex w-full items-center justify-center" + onClick={handleClick} + > + {isHovered && ( + <motion.div + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + transition={{ duration: 0.2 }} + className="absolute z-10 flex flex-col rounded-md bg-zinc-800 p-2 text-sm text-white shadow-md" + style={{ + top: `${position === 'top' ? '-40px' : '40px'}`, + transform: 'translateX(-50%)', + width: 'fit-content', + }} + > + <div className="flex w-full flex-row"> + <img src="/note/link.svg" alt="" /> + <a + href={link} + target="_blank" + className="text-sm text-blue-300 underline" + > + {link} + </a> + </div> + <div + className={`absolute left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-zinc-800 ${position === 'bottom' ? '-top-1' : '-bottom-1'}`} + ></div> + </motion.div> + )} + + <div + className="w-fit cursor-pointer" + onClick={() => setIsHovered((prev) => !prev)} + > + {children} + </div> + </div> + ); +}; +export default HoverLinkChip; diff --git a/frontend/src/components/atoms/PostTextareaInput.tsx b/frontend/src/components/atoms/PostTextareaInput.tsx index b956399aab75e20235eca2e09fcfac94c61f1c68..a8fb7cd0579d5f00eccf71ab777f32b813d77674 100644 --- a/frontend/src/components/atoms/PostTextareaInput.tsx +++ b/frontend/src/components/atoms/PostTextareaInput.tsx @@ -96,7 +96,13 @@ const PostTextareaInput = ({ control, errors }) => { class: 'border', }, }), - Image, + Image.configure({ + allowBase64: true, + inline: true, + HTMLAttributes: { + width: '20%', + }, + }), BulletList, OrderedList, ListItem, diff --git a/frontend/src/components/molecules/MultipartDataForm.tsx b/frontend/src/components/molecules/MultipartDataForm.tsx index 608f614ef8060c8bf468b4788c9b9ab6bc8d79e8..0758b2f24434983e4dbb51ddc3ff8c7f5f9b6e89 100644 --- a/frontend/src/components/molecules/MultipartDataForm.tsx +++ b/frontend/src/components/molecules/MultipartDataForm.tsx @@ -1,17 +1,24 @@ -import React, { Dispatch, SetStateAction, useState } from 'react'; +import React, { useState } from 'react'; import Button from '../atoms/Button'; -//Will be edited in ticket "Add multidata support" -interface Source { - source: 'local' | 'external'; - setSource: Dispatch<SetStateAction<Source | null>>; +import HoverLinkChip from '../atoms/HoverLinkChip'; +interface MultipartDataFormProps { + activeForm: string; + nullify: () => void; + setImagePayload?: (payload: any) => void; + setLinkPayload?: (payload: any) => void; } -const MultipartDataForm = () => { +const MultipartDataForm = ({ + activeForm, + nullify, + setImagePayload, + setLinkPayload, +}: MultipartDataFormProps) => { const [files, setFiles] = useState(null); - const [preview, setPreview] = useState(''); - const [source, setSource] = useState('local'); - const [fileType, setFileType] = useState(''); - const [fileName, setFileName] = useState(''); + const [preview, setPreview] = useState(null); const [fileSize, setFileSize] = useState(''); + const [links, setLinks] = useState(null); + const [currentLinkTitle, setCurrentLinkTitle] = useState(''); + const [currentLinkURL, setCurrentLinkURL] = useState(''); const formatFileSize = (sizeInBytes) => { const kiloBytes = sizeInBytes / 1024; @@ -35,64 +42,68 @@ const MultipartDataForm = () => { const handleDrop = (e) => { preventDefault(e); - const file = e.dataTransfer.files[0]; - if (file) { - setFiles(file); - previewFile(file); + const files = e.dataTransfer.files; + if (files.length > 0) { + setFiles(files); + previewFile(files); } }; const handleFileSelect = async (e) => { - if (source === 'local') { - const file = e.target.files[0]; - const allowedTypes = [ - 'image/png', - 'application/pdf', - 'image/webp', - 'image/jpeg', - 'image/svg', - 'image/jpg', - ]; - - if (file && allowedTypes.includes(file.type)) { - setFiles(file); - previewFile(file); - } else { - console.log('File rejected:', file.type); - } + const files = e.target.files; + const allowedTypes = [ + 'image/png', + 'application/pdf', + 'image/webp', + 'image/jpeg', + 'image/svg', + 'image/jpg', + ]; + console.log(files.length); + if (files && files.length > 0) { + const validFiles = Array.from(files as FileList).filter((file) => + allowedTypes.includes(file.type), + ); + setFiles(validFiles); + previewFile(validFiles); } else { - // Check if the input is a URL - const url = e.target.value; - if (url) { - try { - const response = await fetch(url); - console.log(response); - if (response.ok) { - console.log(response.body.getReader()); - // setFiles(url); - // previewFile(url); - } else { - console.log('Failed to fetch image from URL:', response.status); - } - } catch (error) { - console.error('Error fetching image from URL:', error); + console.log('No files selected'); + } + }; + const handleAddLink = () => { + const title = currentLinkTitle; + const url = currentLinkURL; + if (title && url) { + setLinks((prev) => { + if (prev && prev.length > 0) { + return [...prev, { title, url }]; + } else { + return [{ title, url }]; } - } + }); + setCurrentLinkTitle(''); + setCurrentLinkURL(''); } }; - - const previewFile = (file) => { - const reader = new FileReader(); - reader.addEventListener('load', () => { - setPreview(reader.result as any); + const previewFile = (files) => { + const previews = Array.from(files as FileList).map((file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); }); - if (file) { - reader.readAsDataURL(file); - setFileName(file.name); - setFileType(file.type); - setFileSize(formatFileSize(file.size)); - } + Promise.all(previews).then((results) => { + setPreview((prev) => { + if (prev && prev.length > 0) { + return [...prev, ...results]; + } else { + return results; + } + }); + }); }; const toBase64 = (file: File) => { return new Promise((resolve, reject) => { @@ -110,86 +121,178 @@ const MultipartDataForm = () => { }); }; const sendFile = async () => { - const base64str = (await toBase64(files)) as string; - console.log(base64str); + const base64strArray = await Promise.all( + Array.from(files as FileList).map(async (file) => { + const base64str = (await toBase64(file)) as string; + return base64str; + }), + ); + setFiles(null); + setPreview(null); + setImagePayload(base64strArray); + nullify(); }; + const sendLink = () => { + if (!links || links.length === 0) { + setLinkPayload([{ title: currentLinkTitle, url: currentLinkURL }]); + setCurrentLinkTitle(''); + setCurrentLinkURL(''); + nullify(); + return; + } + if (links && links.length > 0) { + setLinkPayload(links); + setLinks(null); + nullify(); + } + }; + return ( - <div id="overlay" className="min-h-[140px]"> - <div className="mb-4 flex flex-row gap-3"> - <div - onClick={() => setSource('local')} - className={`cursor-pointer border-r-2 p-2 ${ - source === 'local' && 'text-blue-800' - }`} - > - Local - </div> - <div - onClick={() => setSource('external')} - className={`cursor-pointer p-2 ${ - source === 'external' && 'text-blue-800' - }`} - > - External - </div> - </div> - {source === 'local' ? ( + <div className="absolute top-9 z-50 flex min-h-[140px] min-w-96 flex-col rounded-lg bg-white p-5 shadow-lg"> + {activeForm === 'image' ? ( <> + <div className="h-11 text-[22px] font-bold text-zinc-800"> + Add File + </div> <div - className="cursor-pointer border-2 border-dashed border-blue-800 p-5 text-center" + className="flex h-36 items-center justify-center rounded-lg border-2 border-zinc-100 bg-violet-50 px-6 py-4 text-center" onDragOver={preventDefault} onDragEnter={preventDefault} onDrop={handleDrop} + > + <p className="text-[11px] font-medium text-slate-500"> + Drag and drop file + </p> + </div> + <div + className="flex cursor-pointer flex-col items-center justify-center bg-white p-4" onClick={() => document.getElementById('fileInput').click()} > - <p>Click or Drag & Drop your files here</p> + <p className="text-center text-lg font-normal text-blue-800"> + Load from computer + </p> + </div> + <p className="text-start text-sm font-medium text-black"> + Supported file formats: PNG, JPEG, PDF. + <br /> + Max file size 10mb. + </p> + <div className="mt-3 grid grid-cols-2 gap-2"> + {preview && + preview + .slice(0, 4) + .map((image, index) => ( + <img + key={index} + src={image} + alt="Preview Unavailable" + className={`block max-w-20 rounded-lg grid-item-${index + 1}`} + /> + ))} + {preview && preview.length > 4 && ( + <div className="flex items-center justify-center rounded-lg bg-gray-200"> + <p className="font-medium text-gray-600"> + +{preview.length - 4} + </p> + </div> + )} </div> - {preview && ( - <img - src={preview} - alt="Preview Unavailable" - className="mb-3 mt-3 block w-[40%] rounded-lg" - /> - )} <input type="file" name="files" id="fileInput" accept=".png, .pdf, .webp, .jpg, .jpeg, .gif" + multiple={true} onChange={handleFileSelect} style={{ display: 'none' }} /> <br /> - {fileName && <p>File Name: {fileName}</p>} {fileSize && <p>File Size: {fileSize}</p>} - {fileType && <p>File Type: {fileType}</p>} {files && ( - <Button sx={{ marginTop: '10px' }} handleClick={sendFile}> - <p>Add</p> - </Button> + <div className="mt-2 flex flex-col gap-2"> + <Button handleClick={sendFile}> + <p>Add File</p> + </Button> + <Button + handleClick={() => { + nullify(); + setPreview(null); + setFiles(null); + }} + variant="outlined" + bgColor="transparent" + > + <p>Cancel</p> + </Button> + </div> )} </> ) : ( <> - {preview && ( - <img - src={preview} - alt="Preview Unavailable" - className="mb-3 mt-3 block w-[40%] rounded-lg" + <div className="h-11 text-[22px] font-bold text-zinc-800"> + Add a Link + </div> + <div className="flex flex-col gap-1"> + <label>Title</label> + <input + type="text" + className={`w-full rounded-full border border-zinc-100 py-3 pl-5 pr-3`} + data-testid="URL-title-input" + placeholder="Paste or type URL title here" + value={currentLinkTitle} + onChange={(e) => setCurrentLinkTitle(e.target.value)} + /> + </div> + <div className="flex flex-col gap-1"> + <label>URL</label> + <input + type="text" + className={`w-full rounded-full border border-zinc-100 py-3 pl-5 pr-3`} + data-testid="URL-input" + placeholder="Paste or type URL here" + value={currentLinkURL} + onChange={(e) => setCurrentLinkURL(e.target.value)} /> + </div> + <div + className="flex items-center justify-end p-3" + onClick={handleAddLink} + > + <p className="cursor-pointer text-blue-800"> + Add other link</p> + </div> + {links && ( + <div className="flex flex-col gap-2"> + {links.map((link, index) => ( + <div + key={index} + className="flex items-center justify-center rounded-xl" + > + <HoverLinkChip + children={<p className="text-blue-400">{link.title}</p>} + link={link.url} + position="top" + /> + </div> + ))} + </div> )} - <input - type="text" - id="urlInput" - className="rounded-full border border-zinc-100 px-3 py-2" - placeholder="Paste or type URL here" - onChange={handleFileSelect} - /> + <div className="mt-2 flex flex-col gap-2"> + <Button handleClick={sendLink}> + <p>{links && links.length > 1 ? 'Add Links' : 'Add Link'}</p> + </Button> + <Button + handleClick={() => { + nullify(); + setPreview(null); + setFiles(null); + }} + variant="outlined" + bgColor="transparent" + > + <p>Cancel</p> + </Button> + </div> <br /> - {preview && <p>Preview:</p>} - {preview && ( - <img src={preview} alt="Preview" className="h-auto w-40" /> - )} </> )} </div> @@ -197,15 +300,3 @@ const MultipartDataForm = () => { }; export default MultipartDataForm; -// {showOverlay && uploadProgress > 0 && ( -// <div id="overlay"> -// <div className="progress-bar-container"> -// <label htmlFor="file">Sit tight as your post is uploaded:</label> -// <br /> -// <progress id="file" value={uploadProgress} max="100"> -// {uploadProgress}% -// </progress> -// {uploadProgress}% -// </div> -// </div> -// )} diff --git a/frontend/src/components/molecules/NewNotePostForm.tsx b/frontend/src/components/molecules/NewNotePostForm.tsx index 0ae9b19e213bdb688c599da193cbaa9d35474d4a..c27bc2b562e71f9db8e99a1698665377054aba88 100644 --- a/frontend/src/components/molecules/NewNotePostForm.tsx +++ b/frontend/src/components/molecules/NewNotePostForm.tsx @@ -1,9 +1,8 @@ -import React, { useState } from 'react'; +import React from 'react'; import PostInput from '../atoms/PostInput'; import PostSubmitArea from '../atoms/PostSubmitArea'; import PostTextareaInput from '../atoms/PostTextareaInput'; import PostVisiblityInput from '../atoms/PostVisiblityInput'; -import PostTagInput from '../atoms/PostInput'; const NewNotePostForm = ({ register, control, errors }) => { return ( diff --git a/frontend/src/components/molecules/TextEditOptions.tsx b/frontend/src/components/molecules/TextEditOptions.tsx index 980b13ce6aae4d7d7089545197e1c7f5e099219d..e80f5c6da94f498c84f35d3f49a0706bb97f4baf 100644 --- a/frontend/src/components/molecules/TextEditOptions.tsx +++ b/frontend/src/components/molecules/TextEditOptions.tsx @@ -1,6 +1,7 @@ import { Editor } from '@tiptap/react'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import MultipartDataForm from './MultipartDataForm'; +import { set } from 'cypress/types/lodash'; type TextEditOptionsProps = { editor: Editor; active: boolean; @@ -11,6 +12,42 @@ const TextEditOptions: React.FC<TextEditOptionsProps> = ({ active, }) => { const [showOverlay, setShowOverlay] = useState(false); + const [activeForm, setActiveForm] = useState(null); + const [imagePayload, setImagePayload] = useState(null); + const [linkPayload, setLinkPayload] = useState(null); + const resetState = () => { + setShowOverlay(false); + setActiveForm(null); + }; + useEffect(() => { + if (imagePayload) { + if (imagePayload.length > 1) { + imagePayload.forEach((image) => { + console.log(image); + editor.chain().focus().setImage({ src: image }).run(); + }); + setImagePayload(null); + } else { + editor.chain().focus().setImage({ src: imagePayload[0] }).run(); + setImagePayload(null); + } + resetState(); + } + if (linkPayload) { + if (linkPayload.length > 1) { + linkPayload.forEach((link) => { + const linkTag = ` <a href=${link.url}>${link.title}</a> `; + editor.chain().focus().insertContent(linkTag).run(); + }); + setLinkPayload(null); + } else { + const linkTag = ` <a href=${linkPayload[0].url}>${linkPayload[0].title}</a> `; + editor.chain().focus().insertContent(linkTag).run(); + setLinkPayload(null); + } + resetState(); + } + }, [imagePayload, linkPayload]); const handleFormatText = (format) => { //active maps to isEnabled if (!active) return; @@ -29,11 +66,13 @@ const TextEditOptions: React.FC<TextEditOptionsProps> = ({ case 'heading': editor.chain().focus().toggleHeading({ level: 1 }).run(); break; - case 'video': + case 'image': + setShowOverlay((prev) => !prev); + setActiveForm((prev) => (prev === 'image' ? null : 'image')); + break; + case 'link': setShowOverlay((prev) => !prev); - // const previousUrl = editor.getAttributes("link").href; - // const url = window.prompt("URL", previousUrl); - // editor.chain().focus().toggleLink({ href: url }).run(); + setActiveForm((prev) => (prev === 'link' ? null : 'link')); break; default: editor.commands[ @@ -143,21 +182,46 @@ const TextEditOptions: React.FC<TextEditOptionsProps> = ({ > <img src="/note/table.svg" alt="table" /> </div> - <div - className={`flex h-full flex-row items-center justify-center gap-1 text-nowrap rounded-lg bg-violet-50 px-2 text-zinc-800 ${active ? 'cursor-pointer' : 'pointer-events-none'}`} - onClick={() => handleFormatText('image')} - > - <img src="/note/photo.svg" alt="Image" /> - <p>Add File</p> + <div className={`relative flex h-full flex-col`}> + <div + className={`flex h-full flex-row items-center justify-center gap-5 text-nowrap rounded-lg ${activeForm === 'image' ? 'bg-white' : 'bg-violet-50'} px-3 text-zinc-800 ${active ? 'cursor-pointer' : 'pointer-events-none'}`} + onClick={() => handleFormatText('image')} + > + <img + src="/note/photo.svg" + alt="Image" + className={`${activeForm === 'image' ? 'opacity-100' : 'opacity-50'}`} + /> + <p>Add Image</p> + </div> + {showOverlay && activeForm === 'image' && ( + <MultipartDataForm + activeForm={'image'} + nullify={resetState} + setImagePayload={setImagePayload} + /> + )} </div> - <div - className={`flex h-full flex-row items-center justify-center gap-1 text-nowrap rounded-lg bg-violet-50 px-2 text-zinc-800 ${active ? 'cursor-pointer' : 'pointer-events-none'} `} - onClick={() => handleFormatText('link')} - > - <img src="/note/link.svg" alt="Link" /> - <p>Add Link</p> + <div className={`relative flex h-full flex-col`}> + <div + className={`flex h-full flex-row items-center justify-center gap-5 text-nowrap rounded-lg ${activeForm === 'link' ? 'bg-white' : 'bg-violet-50'} px-3 text-zinc-800 ${active ? 'cursor-pointer' : 'pointer-events-none'}`} + onClick={() => handleFormatText('link')} + > + <img + src="/note/link.svg" + alt="Link" + className={`${activeForm === 'link' ? 'opacity-100' : 'opacity-50'}`} + /> + <p>Add Link</p> + </div> + {showOverlay && activeForm === 'link' && ( + <MultipartDataForm + activeForm={'link'} + nullify={resetState} + setLinkPayload={setLinkPayload} + /> + )} </div> - {showOverlay && <MultipartDataForm />} </div> ); };