From 30fdc7cb1dc6ed0710b61da8141ea5db21599271 Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Fri, 23 Aug 2024 06:42:27 -0700 Subject: [PATCH] [WIP] v1 UI for custom copyright (#418) Added custom copyright --- src/atoms/input/Checkbox.jsx | 4 +- src/atoms/input/Input.tsx | 7 +- src/atoms/input/TextArea.jsx | 2 +- src/atoms/input/index.module.scss | 5 +- src/atoms/layout/page/index.module.scss | 1 + src/atoms/modal/InfoModal.tsx | 36 ++ src/atoms/modal/index.module.scss | 39 ++ src/atoms/modal/index.ts | 1 + src/components/form/CustomCopyrightForm.jsx | 566 ++++++++++++++++++++ src/components/form/FormFields.jsx | 14 + src/components/form/copyrightmodaltext.ts | 71 +++ src/components/form/index.module.scss | 13 +- src/components/media-types/text/index.jsx | 1 + src/components/preview/index.jsx | 18 +- src/constants.ts | 2 +- src/context/mintStore.ts | 20 +- src/data/ipfs.ts | 27 +- src/index.jsx | 2 + src/pages/mint/fields.js | 18 +- src/pages/objkt-display/index.tsx | 4 + src/pages/objkt-display/tabs/Copyright.jsx | 160 ++++++ src/pages/objkt-display/tabs/Info.jsx | 5 +- src/pages/objkt-display/tabs/index.js | 1 + src/styles/main.scss | 16 + 24 files changed, 989 insertions(+), 44 deletions(-) create mode 100644 src/atoms/modal/InfoModal.tsx create mode 100644 src/atoms/modal/index.module.scss create mode 100644 src/atoms/modal/index.ts create mode 100644 src/components/form/CustomCopyrightForm.jsx create mode 100644 src/components/form/copyrightmodaltext.ts create mode 100644 src/pages/objkt-display/tabs/Copyright.jsx diff --git a/src/atoms/input/Checkbox.jsx b/src/atoms/input/Checkbox.jsx index 1d25148c6..9f78dafed 100644 --- a/src/atoms/input/Checkbox.jsx +++ b/src/atoms/input/Checkbox.jsx @@ -38,12 +38,13 @@ const Checkbox = forwardRef( const handleCheck = useCallback( (e) => { + if (disabled) return const c = e.target.checked setChecked(c) onCheck?.(c) }, // eslint-disable-next-line react-hooks/exhaustive-deps - [checked] + [checked, disabled] ) const classes = classNames({ @@ -68,6 +69,7 @@ const Checkbox = forwardRef( onWheel={onWheel} checked={checkedProp} aria-checked={checked} + disabled={disabled} /> diff --git a/src/atoms/input/Input.tsx b/src/atoms/input/Input.tsx index 848b5f344..acf553046 100644 --- a/src/atoms/input/Input.tsx +++ b/src/atoms/input/Input.tsx @@ -37,7 +37,7 @@ type InputType = interface InputProps { type: InputType placeholder: string - name?: string + name?: string | '' min?: number max?: number maxlength?: number @@ -82,10 +82,7 @@ function Input( const handleInput = useCallback( (e: React.FormEvent) => { - if (ref) { - onChange(e) - return - } + onChange(e) const target = e.target as HTMLInputElement if (target) { const v = diff --git a/src/atoms/input/TextArea.jsx b/src/atoms/input/TextArea.jsx index cd6befb9d..402665695 100644 --- a/src/atoms/input/TextArea.jsx +++ b/src/atoms/input/TextArea.jsx @@ -10,7 +10,7 @@ const Textarea = forwardRef( min, max, children, - maxlength = 5000, + maxlength = 50000, label, onChange = () => null, onBlur = () => null, diff --git a/src/atoms/input/index.module.scss b/src/atoms/input/index.module.scss index 67666f4cc..6a7e97b84 100644 --- a/src/atoms/input/index.module.scss +++ b/src/atoms/input/index.module.scss @@ -83,6 +83,7 @@ &:focus { outline: none; + border: 1px solid var(--gray-40); } &:focus::placeholder { @@ -91,7 +92,9 @@ } textarea { - min-height: 75px; + min-height: 100px; // Increased minimum height + height: auto; // Allow the height to be determined by content + resize: vertical; // Allow vertical resizing } } } diff --git a/src/atoms/layout/page/index.module.scss b/src/atoms/layout/page/index.module.scss index 3705faca3..a343e50ce 100644 --- a/src/atoms/layout/page/index.module.scss +++ b/src/atoms/layout/page/index.module.scss @@ -8,6 +8,7 @@ min-height: 100vh; // min-width: 320px; width: 100%; + padding: 1em 0; display: flex; flex-direction: column; diff --git a/src/atoms/modal/InfoModal.tsx b/src/atoms/modal/InfoModal.tsx new file mode 100644 index 000000000..e106ec55f --- /dev/null +++ b/src/atoms/modal/InfoModal.tsx @@ -0,0 +1,36 @@ +// InfoModal.tsx +import React, { useEffect } from 'react'; +import styles from '@style' + +interface InfoModalProps { + isOpen: boolean; + title: string; + content: string; + onClose: () => void; // Function to toggle visibility +} + +export const InfoModal: React.FC = ({ isOpen, title, content, onClose }) => { + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if (event.target === document.getElementById('modal-overlay')) { + onClose(); + } + }; + + document.addEventListener('mousedown', handleOutsideClick); + return () => document.removeEventListener('mousedown', handleOutsideClick); + }, [onClose]); + + if (!isOpen) return null; + + return ( + + ); +}; + +export default InfoModal; diff --git a/src/atoms/modal/index.module.scss b/src/atoms/modal/index.module.scss new file mode 100644 index 000000000..5ca167899 --- /dev/null +++ b/src/atoms/modal/index.module.scss @@ -0,0 +1,39 @@ +@import '@styles/variables.scss'; +@import '@styles/layout.scss'; + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.382); + display: flex; + justify-content: center; + align-items: center; + z-index: 99; + overflow-y: auto; +} + +.modalContent { + background-color: var(--background-color); + padding: 20px; + box-shadow: 1px 1px var(--text-color); + width: 80%; + max-width: 61.8%; + height: auto; + max-height: 61.8vh; + overflow: auto; + margin-top: 0 auto; +} + +.modalContent div { + margin: 1em 0; +} + +.modalContent p { + margin-top: 1em; +} +.modalContent li { + margin-top: 1em; +} diff --git a/src/atoms/modal/index.ts b/src/atoms/modal/index.ts new file mode 100644 index 000000000..c9f66ef4a --- /dev/null +++ b/src/atoms/modal/index.ts @@ -0,0 +1 @@ +export { InfoModal } from './InfoModal' \ No newline at end of file diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx new file mode 100644 index 000000000..4efd24418 --- /dev/null +++ b/src/components/form/CustomCopyrightForm.jsx @@ -0,0 +1,566 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import React, { useState, useEffect, useCallback } from 'react' +import { Checkbox, Input, Textarea } from '@atoms/input' +import ReactSelect from 'react-select' +import styles from '@style' +import { style as select_style, theme } from '../../atoms/select/styles' +import { useOutletContext } from 'react-router' +import { useMintStore } from '@context/mintStore' +import { copyrightModalText } from './copyrightmodaltext' +import { InfoModal } from '@atoms/modal' + +const initialClauses = { + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', // Options are 'none', 'majority', 'superMajority' + retainCreatorRights: true, // When exclusive rights conditions are met, does the Creator retain their rights to their own work? + releasePublicDomain: false, + customUriEnabled: false, + customUri: '', + addendum: '', +} + +const clauseLabels = { + reproduce: 'Right to Reproduce', + broadcast: 'Right to Broadcast', + publicDisplay: 'Right to Public Display', + createDerivativeWorks: 'Right to Create Derivative Works', + exclusiveRights: 'Exclusive Rights Based on Ownership Share', + retainCreatorRights: 'Creator Retains Rights Even When Exclusive', + releasePublicDomain: 'Release to Public Domain', + customUriEnabled: 'Custom URI', + overview: 'Copyright Overview', +} + +export const exclusiveRightsOptions = [ + { value: 'none', label: ' 🚫 None (No Exclusive Rights To Any Party)' }, + { + value: 'majority', + label: ' βš–οΈ Majority Share (50%+ Editions Owned = Exclusive Rights)', + }, + { + value: 'superMajority', + label: + ' βš–οΈ Super-Majority Share (66.667%+ Editions Owned = Exclusive Rights)', + }, +] + +export const ClausesDescriptions = ({ clauses }) => { + const descriptions = { + reproduce: { + true: 'βœ… Yes', + false: '🚫 No', + }, + broadcast: { + true: 'βœ… Yes', + false: '🚫 No', + }, + publicDisplay: { + true: 'βœ… Yes', + false: '🚫 No', + }, + createDerivativeWorks: { + true: 'βœ… Yes', + false: '🚫 No', + }, + releasePublicDomain: { + true: 'βœ… Yes', + false: '🚫 No', + }, + customUriEnabled: { + true: 'πŸ“ Yes', + false: '🚫 No', + }, + retainCreatorRights: { + true: 'βœ… Yes', + false: '⚠️ No', + }, + } + + return ( +
+ Copyright Permissions Granted on Ownership: +
    + {clauses.customUriEnabled ? ( + <> +
  • Custom URI Enabled: {descriptions.customUriEnabled[true]}
  • +
  • Custom URI: {clauses?.customUri || 'No URI Set'}
  • + + ) : ( + Object.entries(clauses).map(([key, value]) => { + if (key === 'exclusiveRights') { + const exclusiveLabel = + exclusiveRightsOptions.find((option) => option.value === value) + ?.label || 'None' + return
  • Exclusive Rights: {exclusiveLabel}
  • + } else if (key === 'customUri') { + return null + } else if (key === 'addendum') { + return
  • Addendum: {value ? 'βœ… Yes' : '🚫 No'}
  • + } else { + const displayValue = + descriptions[key]?.[value] || 'Unknown Status' + return ( +
  • + {clauseLabels[key]}: {displayValue} +
  • + ) + } + }) + )} +
+
+
+ ) +} + +function CustomCopyrightForm({ onChange, value }) { + const { license, minterName, address } = useOutletContext() + const [clauses, setClauses] = useState(initialClauses) + const [generatedDocument, setGeneratedDocument] = useState( + 'No Permissions Chosen' + ) + const [documentText, setDocumentText] = useState('No Permissions Chosen') // necessary for State management in parent element + const [uriError, setUriError] = useState('') + + const updateCustomLicenseData = useMintStore( + (state) => state.updateCustomLicenseData + ) + + let clauseNumber = 1 + + const generateDocumentText = useCallback(() => { + let minterInfo = minterName ? `[${minterName}, ${address}]` : `[${address}]` + let documentText = `This Custom License Agreement ("Agreement") is granted by the creator ("Creator") of the Non-Fungible Token ("NFT") identified by the owner of wallet address ${minterInfo} ("Wallet Address"). This Agreement outlines the rights and obligations associated with the ownership and use of the NFT's likeness and any derivatives thereof ("Work"). +\nβ€œEditions” refers to the total number of authorized copies of the NFT that the Creator issues at the time of minting. Each copy represents an "Edition" of the NFT, allowing multiple Owners (or one Owner holding multiple copies) to hold rights to the Work under the terms of this Agreement.` + + documentText += `\n\nIn cases where multiple Creators or Collaborators have contributed to the creation of the Work, the rights and obligations stipulated herein apply equally to all Creators. Each Creator is entitled to the rights granted under this Agreement, and such rights are shared collectively among all Creators unless specified otherwise.` + + if (clauses.releasePublicDomain) { + documentText += `\n\n${clauseNumber++}. Release to Public Domain: +The Creator hereby releases all copyright and related rights, title, and interest in and to the Work into the public domain, free from any copyright restrictions. This release applies globally, allowing for the free use, reproduction, and modification of the Work without any compensation due to the Creator.` + } else { + if (clauses.reproduce) { + documentText += `\n\n${clauseNumber++}. Right to Reproduce: +The Creator hereby grants to each owner of the NFT ("Owner") a worldwide license to use the Work for both commercial and non-commercial reproduction purposes.` + } + if (clauses.broadcast) { + documentText += `\n\n${clauseNumber++}. Right to Broadcast: +The Creator grants to each Owner a worldwide license to use the Work for broadcasting purposes for both commercial and non-commercial use.` + } + if (clauses.publicDisplay) { + documentText += `\n\n${clauseNumber++}. Right to Public Display: +The Creator grants to each Owner a worldwide license to publicly display the Work, either as a physical display or as a performance for live events. This license does not permit the monetization of the Work by the Owner and requires the Owner to provide full attribution to the Creator.` + } + if (clauses.createDerivativeWorks) { + documentText += `\n\n${clauseNumber++}. Right to Create Derivative Works: +The Creator grants to each Owner a worldwide license to publicly display the Work, either as a physical display or as a performance for live events. This license does not permit the monetization of the Work by the Owner and requires the Owner to provide full attribution to the Creator.` + } + } + + // contract defaults to "All Rights Reserved" where nothing is chosen + if ( + !clauses.publicDisplay && + !clauses.reproduce && + !clauses.broadcast && + !clauses.createDerivativeWorks && + !clauses.releasePublicDomain + ) { + documentText += `\n\n${clauseNumber++}. All Rights Reserved: +No rights are granted under this Agreement. All rights for the Work are reserved solely by the Creator.` + } + + if (clauses.exclusiveRights === 'none') { + documentText += `\n\n${clauseNumber++}. Exclusive Rights: +No exclusive rights are granted under this Agreement. All rights are non-exclusive and shared among all rightful owners or licensees - or in cases of "All Rights Reserved", exclusivity is granted solely to the Creator themselves.` + } else if ( + clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks + ) { + // at least one rights clause must be picked for exclusive rights to be an option + const rightsDescription = + clauses.exclusiveRights === 'majority' + ? 'majority share (over 50%)' + : 'super-majority share (66.667%+ or exactly 2/3rds)' + documentText += `\n\n${clauseNumber++}. Exclusive Rights: +The Creator grants exclusive rights as outlined in this Agreement to the Owner(s) holding a ${rightsDescription} of the editions of the NFT at the time of its original mint. If no single party holds such a share, the rights outlined in this Agreement apply non-exclusively to all Owners. (In some cases, the Creator themselves may be the exclusive Owner.)` + } + + if (clauses.exclusiveRights !== 'none' && clauses.retainCreatorRights) { + documentText += `\n\n${clauseNumber++}. Retention of Creator's Rights: +Despite reaching the threshold for exclusive rights, the Creator retains certain rights as specified under this Agreement, even if exclusivity conditions are met by other Owners. The rights are then split equally between the Creator and Owner which has been granted exclusive rights over the other Owner(s) of the Work, effective immediately after the date in which the condition for exclusivity has been met.` + } + + documentText += `\n\n${clauseNumber++}. Jurisdiction and Legal Authority: +This Agreement is subject to and shall be interpreted in accordance with the laws of the jurisdiction in which the Creator and Owner(s) are domiciled. The rights granted hereunder are subject to any applicable international, national, and local copyright and distribution laws.` + + documentText += `\n\n${clauseNumber++}. Identification and Representation: +Each Owner (including the Creator and all Collaborators) affirms that the wallet address provided is under their control or under the control of the party they legitimately represent. Each Owner accepts all responsibility for validating their ownership or representative authority of the said wallet address. It is the Owner's duty to provide satisfactory proof of ownership or authorization as may be required to establish their connection to the wallet address in question. Failure to conclusively demonstrate such ownership or authority may result in denial of access to services, rights, or privileges associated with the wallet address under this Agreement.` + + documentText += `\n\n${clauseNumber++}. Amendments and Modifications: +This Agreement may be amended or modified only by a written document signed by both the Creator and the Owner(s) holding the relevant majority or super-majority share, as applicable.` + + documentText += `\n\n${clauseNumber++}. Proof of Ownership and Responsibility: +Each individual claiming ownership ("Claimant") must conclusively prove that they are the legitimate Owner or Creator of the specified wallet address associated with this Agreement. It is the sole responsibility of the Claimant to provide irrefutable evidence supporting their claim. This proof may include, but is not limited to, cryptographic signatures, transaction histories, and other blockchain-based verifications that establish an undeniable link between the Claimant and the wallet address in question. Failure to provide satisfactory evidence will result in the denial of any rights, privileges, or access purportedly associated with the wallet address under the terms of this Agreement.` + + documentText += `\n\n${clauseNumber++}. Limitation of Platform Responsibility: +This Agreement is entered into solely between the Creator and the Owner(s) of the Non-Fungible Token ("NFT") and the associated digital or physical artwork ("Work"). TEIA (teia.art), formally operating under TEIA DAO LLC, and its affiliated members, collectively referred to as "Platform," do not bear any responsibility for the enforcement, execution, or maintenance of this Agreement. The Platform serves only as a venue for the creation, display, and trading of NFTs and does not participate in any legal relationships established under this Agreement between the Creator and the Owner(s). All responsibilities related to the enforcement and adherence to the terms of this Agreement rest solely with the Creator and the Owner(s). The Platform disclaims all liability for any actions or omissions of any user related to the provisions of this Agreement.` + + documentText += `\n\n${clauseNumber++}. Perpetuity of Agreement: +This Agreement remains effective in perpetuity as long as the Owner(s) can conclusively demonstrate proof of ownership of the NFT representing the Work, beyond reasonable doubt. Proof of ownership must be substantiated through reliable and verifiable means, which may include, but are not limited to, transaction records, cryptographic proofs, or any other blockchain-based evidence that unequivocally establishes ownership. This perpetual license ensures that the rights and privileges granted under this Agreement persist as long as the ownership criteria are met and validated.` + + documentText += `\n\n${clauseNumber++}. Transfer of Rights Upon Change of Ownership: +The rights and obligations stipulated in this Agreement, along with any associated privileges, shall transfer automatically to a new owner upon the change of ownership from one wallet to another. This transfer is triggered by the sale, gift, or any form of transfer of the NFT that embodies the Work. The transfer of rights becomes effective immediately following the timestamp of the transaction recorded on the blockchain. It is incumbent upon the new Owner to verify and uphold the terms set forth in this Agreement, ensuring continuity and adherence to the stipulated conditions. The previous Owner's rights under this Agreement cease concurrently with the transfer of ownership.` + + // Additional notes based on user input + if (clauses.addendum) { + documentText += `\n\nAddendum By Creator:\n${clauses?.addendum}` + } + + return documentText + }, [address, clauseNumber, clauses, minterName]) + + // logic for checkboxes + const handleChange = useCallback((value, name) => { + const newValue = Object.prototype.hasOwnProperty.call(value, 'value') + ? value.value + : value + + if (name === 'customUriEnabled') { + if (newValue) { + setClauses((prev) => ({ + ...prev, + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', + retainCreatorRights: true, + releasePublicDomain: false, + customUriEnabled: true, + })) + } else { + setClauses((prev) => ({ + ...prev, + customUriEnabled: false, + customUri: prev.customUri, + })) + } + } + // Handle 'Release to Public Domain' logic + else if (name === 'releasePublicDomain' && newValue === true) { + setClauses({ + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', + retainCreatorRights: true, + releasePublicDomain: true, + customUriEnabled: false, + customUri: '', + }) + } else { + // Normal handling for other checkboxes + setClauses((prev) => ({ + ...prev, + [name]: newValue, + ...(name !== 'releasePublicDomain' && prev.releasePublicDomain + ? { releasePublicDomain: false } + : null), + })) + } + }, []) + + const handleUriChange = (eventOrValue) => { + const value = eventOrValue.target ? eventOrValue.target.value : eventOrValue + + function isValidURI(uri) { + try { + new URL(uri) + return true + } catch (error) { + return false + } + } + if (isValidURI(value)) { + setClauses((prev) => ({ + ...prev, + customUri: value, + })) + } else { + // Handle error state, perhaps set an error message in state + console.error('Invalid URI') + } + } + + // Addendum handling + const handleInputChange = (eventOrValue) => { + let name, value + + if (eventOrValue.target) { + name = eventOrValue.target.name + value = eventOrValue.target.value + } else { + name = 'addendum' + value = eventOrValue + } + + setClauses((prev) => ({ + ...prev, + [name]: value, + })) + } + + // Logic for metadata and document updates + useEffect(() => { + let documentText + const hasActiveRights = + clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks || + clauses.releasePublicDomain + + if (clauses.customUriEnabled) { + documentText = `Custom URI: ${clauses.customUri}` + } else if (!hasActiveRights) { + documentText = 'No Permissions Chosen' + } else { + documentText = generateDocumentText() + } + + setDocumentText(documentText) + setGeneratedDocument(documentText) + updateCustomLicenseData({ clauses: clauses, documentText: documentText }) + }, [ + clauses?.reproduce, + clauses?.broadcast, + clauses?.publicDisplay, + clauses?.createDerivativeWorks, + clauses?.exclusiveRights, + clauses?.releasePublicDomain, + handleChange, + generateDocumentText, + updateCustomLicenseData, + ]) + + // sync to parent State management + useEffect(() => { + onChange({ clauses, documentText }) + }, [clauses, documentText, onChange]) + + const propertiesWithCheckboxes = [ + 'reproduce', + 'broadcast', + 'publicDisplay', + 'createDerivativeWorks', + 'releasePublicDomain', + ] + + // Info Modal + const [modalState, setModalState] = useState({ + isOpen: false, + title: '', + content: '', + }) + + const handleModalOpen = (clauseName) => { + const modalContent = copyrightModalText[clauseName] + + setModalState({ + isOpen: true, + title: clauseLabels[clauseName], + content: modalContent || 'No detailed information available.', + }) + } + + const handleKeyPress = (event, title) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + handleModalOpen(title) + } + } + + return ( +
+

+ Custom License Generation Form + handleModalOpen('overview')} + onKeyPress={(event) => + handleKeyPress(event, clauseLabels['overview']) + } + > + (?) + +

+
+ {propertiesWithCheckboxes.map((clauseName) => ( +
+ handleChange(checked, clauseName)} + disabled={ + clauses.customUriEnabled || + (clauses.releasePublicDomain && + clauseName !== 'releasePublicDomain') + } + /> + handleModalOpen(clauseName)} + onKeyPress={(event) => + handleKeyPress(event, clauseLabels[clauseName]) + } + > + (?) + +
+ ))} + {modalState.isOpen && ( + + } + onClose={() => + setModalState((prev) => ({ ...prev, isOpen: false })) + } + /> + )} +
+
+

{clauseLabels.exclusiveRights}

+ handleModalOpen('exclusiveRights')} + onKeyPress={(event) => + handleKeyPress(event, clauseLabels['exclusiveRights']) + } + > + (?) + +
+ option.value === clauses.exclusiveRights + )} + onChange={(selectedOption) => + handleChange(selectedOption, 'exclusiveRights') + } + placeholder="Select Ownership Share Type" + isDisabled={ + clauses.customUriEnabled || + !Object.values(clauses).some((value) => value && value !== 'none') + } + styles={select_style} + theme={theme} + className={styles.container} + classNamePrefix="react_select" + /> + {['majority', 'superMajority'].includes(clauses.exclusiveRights) && ( // Creator rights retention generated only when exclusive is chosen + + handleChange(checked, 'retainCreatorRights') + } + className={styles.field} + /> + )} +
+ {(clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks || + clauses.releasePublicDomain) && ( +
+

Addendum/Notes

+