From d9bbcece25dd220c5dcb690e3d127677136ece7f Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Wed, 10 Jul 2024 11:07:02 -0700 Subject: [PATCH 01/23] v1 UI for custom copyright --- src/components/form/CustomCopyrightForm.jsx | 221 ++++++++++++++++++++ src/components/form/FormFields.jsx | 4 + src/pages/mint/fields.js | 12 +- 3 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 src/components/form/CustomCopyrightForm.jsx diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx new file mode 100644 index 000000000..a13a670ea --- /dev/null +++ b/src/components/form/CustomCopyrightForm.jsx @@ -0,0 +1,221 @@ +import React, { useState, useEffect } from 'react' +import { Checkbox } 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' + +const initialClauses = { + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', // Options are 'none', 'majority', 'superMajority' + releasePublicDomain: false, +} + +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', + releasePublicDomain: 'Release to Public Domain', +} + +const exclusiveRightsOptions = [ + { value: 'none', label: ' None (No Exclusive Rights To Any Party)' }, + { value: 'majority', label: ' Majority Share (50%+ Editions Owned)' }, + { + value: 'superMajority', + label: ' Super-Majority Share (66.667%+ Editions Owned)', + }, +] + +function CustomCopyrightForm() { + const { license, minterName, address } = useOutletContext() + const [clauses, setClauses] = useState(initialClauses) + const [generatedDocument, setGeneratedDocument] = useState('') + + let clauseNumber = 1 + + useEffect(() => { + // if none of the rights are selected, exclusive rights is set to "none" + const hasActiveRights = + clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks + if (!hasActiveRights || clauses.releasePublicDomain) { + setClauses((prev) => ({ ...prev, exclusiveRights: 'none' })) + } + + 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.` + + 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.)` + } + + 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) 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.` + setGeneratedDocument(documentText) + }, [address, clauseNumber, clauses, minterName]) + + const handleChange = (value, name) => { + const newValue = Object.prototype.hasOwnProperty.call(value, 'value') + ? value.value + : value + + // If 'Release to Public Domain' is checked, disable and reset other clauses + if (name === 'releasePublicDomain' && newValue === true) { + setClauses({ + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', + releasePublicDomain: true, + }) + } else { + setClauses((prev) => ({ + ...prev, + [name]: newValue, + ...(name !== 'releasePublicDomain' && prev.releasePublicDomain + ? { releasePublicDomain: false } + : null), + })) + } + } + + return ( +
+

Custom License Generation Form

+
+ {Object.keys(clauses) + .filter((name) => name !== 'exclusiveRights') + .map((clauseName) => ( + handleChange(checked, clauseName)} + disabled={ + clauseName === 'releasePublicDomain' + ? clauses.releasePublicDomain + : false + } + /> + ))} +
+ + option.value === clauses.exclusiveRights + )} + onChange={(selectedOption) => + handleChange(selectedOption, 'exclusiveRights') + } + placeholder="Select Ownership Share Type" + isDisabled={ + !Object.values(clauses).some( + (value) => value === true && value !== 'none' + ) + } + styles={select_style} + theme={theme} + className={styles.container} + classNamePrefix="react_select" + /> +
+ +
+

Custom License Agreement

+
+          {generatedDocument}
+        
+
+
+ ) +} + +export default CustomCopyrightForm diff --git a/src/components/form/FormFields.jsx b/src/components/form/FormFields.jsx index e0c53d790..0de986e67 100644 --- a/src/components/form/FormFields.jsx +++ b/src/components/form/FormFields.jsx @@ -10,6 +10,7 @@ import { } from '@constants' import { Controller } from 'react-hook-form' import classNames from 'classnames' +import CustomCopyrightForm from './CustomCopyrightForm' const FieldError = memo(({ error, text }) => { const classes = classNames({ @@ -153,6 +154,9 @@ export const FormFields = ({ value, field, error, register, control }) => { /> ) + case 'customCopyrightForm': + return + default: return (

diff --git a/src/pages/mint/fields.js b/src/pages/mint/fields.js index 07ee12c09..e8d844311 100644 --- a/src/pages/mint/fields.js +++ b/src/pages/mint/fields.js @@ -111,17 +111,7 @@ export const fields = [ label: 'Custom license URI', name: 'custom_license_uri', enable_if: 'useCustomLicense', - placeholder: 'The URI to the custom license', - type: 'text', - rules: { - required: true, - valueAs: (f) => f.value, - pattern: { - value: - /((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})|(ipfs:\/\/.*))/g, - message: 'Invalid url (supports http, https or ipfs)', - }, - }, + type: 'customCopyrightForm', }, { label: 'Language', From c5f764428913f8b451ca563cde584e97745f1d72 Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Thu, 11 Jul 2024 13:18:41 -0700 Subject: [PATCH 02/23] [WIP] Custom License Object Data --- src/components/form/CustomCopyrightForm.jsx | 99 +++++++++++++++++---- src/components/form/FormFields.jsx | 12 ++- src/components/preview/index.jsx | 18 +++- src/constants.ts | 2 +- src/context/mintStore.ts | 21 +++-- src/pages/mint/fields.js | 6 +- 6 files changed, 127 insertions(+), 31 deletions(-) diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx index a13a670ea..721049122 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -1,9 +1,10 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { Checkbox } 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' const initialClauses = { reproduce: false, @@ -32,24 +33,65 @@ const exclusiveRightsOptions = [ }, ] +export const ClausesDescriptions = ({ clauses }) => { + const descriptions = { + reproduce: { + true: 'Right to Reproduction: βœ…', + false: 'Right to Reproduction: 🚫', + }, + broadcast: { + true: 'Right to Broadcast: βœ…', + false: 'Right to Broadcast: 🚫', + }, + publicDisplay: { + true: 'Right to Public Display: βœ…', + false: 'Right to Public Display: 🚫', + }, + createDerivativeWorks: { + true: 'Right to Create Derivative Works: βœ…', + false: 'Right to Create Derivative Works: 🚫', + }, + releasePublicDomain: { + true: 'Released to Public Domain: βœ…', + false: 'Released to Public Domain: 🚫', + }, + } + + return ( +

+ Copyright Permissions Granted on Ownership: +
    + {Object.entries(clauses).map(([key, value]) => { + if (key === 'exclusiveRights') { + return ( +
  • + Exclusive Rights:{' '} + {value !== 'none' + ? `Ownership share based on ${value}` + : 'None'} +
  • + ) + } + return
  • {descriptions[key][value]}
  • + })} +
+
+
+ ) +} + function CustomCopyrightForm() { const { license, minterName, address } = useOutletContext() const [clauses, setClauses] = useState(initialClauses) const [generatedDocument, setGeneratedDocument] = useState('') - let clauseNumber = 1 + const updateCustomLicenseData = useMintStore( + (state) => state.updateCustomLicenseData + ) - useEffect(() => { - // if none of the rights are selected, exclusive rights is set to "none" - const hasActiveRights = - clauses.reproduce || - clauses.broadcast || - clauses.publicDisplay || - clauses.createDerivativeWorks - if (!hasActiveRights || clauses.releasePublicDomain) { - setClauses((prev) => ({ ...prev, exclusiveRights: 'none' })) - } + 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.` @@ -120,16 +162,18 @@ Each individual claiming ownership ("Claimant") must conclusively prove that the 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.` - setGeneratedDocument(documentText) + + return documentText }, [address, clauseNumber, clauses, minterName]) - const handleChange = (value, name) => { + const handleChange = useCallback((value, name) => { const newValue = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value : value // If 'Release to Public Domain' is checked, disable and reset other clauses if (name === 'releasePublicDomain' && newValue === true) { + console.log('resetting bc public domain') setClauses({ reproduce: false, broadcast: false, @@ -139,6 +183,7 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th releasePublicDomain: true, }) } else { + console.log('setting clauses in else CCForm') setClauses((prev) => ({ ...prev, [name]: newValue, @@ -147,12 +192,32 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th : null), })) } - } + }, []) + + useEffect(() => { + const hasActiveRights = + clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks + const documentText = generateDocumentText() + + setGeneratedDocument(documentText) + updateCustomLicenseData({ clauses: clauses, documentText }) + }, [ + clauses?.reproduce, + clauses?.broadcast, + clauses?.publicDisplay, + clauses?.createDerivativeWorks, + clauses?.exclusiveRights, + clauses?.releasePublicDomain, + handleChange, + ]) return (

Custom License Generation Form

-
+
{Object.keys(clauses) .filter((name) => name !== 'exclusiveRights') .map((clauseName) => ( @@ -192,7 +257,7 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th classNamePrefix="react_select" />
-
+
{ ) case 'customCopyrightForm': - return + return ( + ( + + )} + /> + ) default: return ( diff --git a/src/components/preview/index.jsx b/src/components/preview/index.jsx index 6543c54f0..771b85a6e 100644 --- a/src/components/preview/index.jsx +++ b/src/components/preview/index.jsx @@ -7,6 +7,7 @@ import { motion } from 'framer-motion' import { useMintStore } from '@context/mintStore' import { Button } from '@atoms/button' import useSettings from '@hooks/use-settings' +import { ClausesDescriptions } from '@components/form/CustomCopyrightForm' function isHTML(mimeType) { return ( mimeType === MIMETYPE.ZIP || @@ -46,7 +47,7 @@ export const Preview = () => { artifact, cover, license, - custom_license_uri, + customLicenseData, royalties, language, photosensitiveSeizureWarning, @@ -60,7 +61,7 @@ export const Preview = () => { st.artifact, st.cover, st.license, - st.custom_license_uri, + st.customLicenseData, st.royalties, st.language, st.photosensitive, @@ -69,6 +70,8 @@ export const Preview = () => { st.isMonoType, ]) + console.log('customLicenseData in Preview', customLicenseData) + const { ignoreUriMap } = useSettings() const token_tags = tags ? tags === '' @@ -89,6 +92,7 @@ export const Preview = () => { ) } + return ( {
- + {customLicenseData?.clauses && license?.label === 'Custom License' && ( +
+ + +
+ )} {(photosensitiveSeizureWarning || nsfw) && ( diff --git a/src/constants.ts b/src/constants.ts index b0148dd17..4219d349b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -238,7 +238,7 @@ export const LICENSE_TYPES: { [key: string]: string } = { 'cc-by-nc-4.0': 'CC BY-NC 4.0 (Attribution-NonCommercial)', 'cc-by-nc-sa-4.0': 'CC BY-NC-SA 4.0 (Attribution-NonCommercial-ShareAlike)', 'cc-by-nc-nd-4.0': 'CC BY-NC-ND 4.0 (Attribution-NonCommercial-NoDerivs)', - custom: 'Custom (Specify below)', + custom: 'Custom License', } export const LICENSE_TYPES_OPTIONS = Object.keys(LICENSE_TYPES).map((k) => ({ diff --git a/src/context/mintStore.ts b/src/context/mintStore.ts index 977b4253a..234c2ae1b 100644 --- a/src/context/mintStore.ts +++ b/src/context/mintStore.ts @@ -38,7 +38,7 @@ export interface MintState { editions?: number royalties?: number license?: SelectField - custom_license_uri?: string + customLicenseData?: any language?: SelectField nsfw: boolean photosensitive: boolean @@ -60,10 +60,11 @@ const defaultValuesStored = { editions: undefined, royalties: undefined, license: undefined, - custom_license_uri: '', + customLicenseData: undefined, language: undefined, nsfw: false, photosensitive: false, + isTyped: false, } const defaultValues = { @@ -71,6 +72,7 @@ const defaultValues = { cover: undefined, thumbnail: undefined, getValuesStored: () => {}, + updateCustomLicenseData: () => {}, } export const useMintStore = create()( @@ -80,6 +82,14 @@ export const useMintStore = create()( ...defaultValuesStored, ...defaultValues, isValid: false, + updateCustomLicenseData: (data) => { + set((state) => ({ + ...state, + customLicenseData: data // directly setting the new data + })); + console.log("Custom license data updated in mintStore:", get().customLicenseData); + } + , reset: () => { set({ ...defaultValuesStored, ...defaultValues }) }, @@ -99,7 +109,7 @@ export const useMintStore = create()( editions, royalties, license, - custom_license_uri, + customLicenseData, artifact, title, description, @@ -268,7 +278,7 @@ export const useMintStore = create()( thumbnail: used_thumb, generateDisplayUri: true, rights: license?.value, - rightUri: custom_license_uri, + rightUri: customLicenseData, language: language?.value, accessibility, contentRating, @@ -284,9 +294,8 @@ export const useMintStore = create()( file: artifact, cover: used_cover, thumbnail: used_thumb, - rights: license?.value, - rightUri: custom_license_uri, + rightUri: customLicenseData, language: language?.value, accessibility, contentRating, diff --git a/src/pages/mint/fields.js b/src/pages/mint/fields.js index e8d844311..7ac1abd06 100644 --- a/src/pages/mint/fields.js +++ b/src/pages/mint/fields.js @@ -14,7 +14,7 @@ export const defaultValues = { editions: '', royalties: '', license: '', - custom_license_uri: '', + customLicenseData: {}, language: '', nsfw: false, photosensitive: false, @@ -108,8 +108,8 @@ export const fields = [ options: LICENSE_TYPES_OPTIONS, }, { - label: 'Custom license URI', - name: 'custom_license_uri', + label: 'Custom License', + name: 'customLicenseData', enable_if: 'useCustomLicense', type: 'customCopyrightForm', }, From b8be3325054dbc0ded46704fe3d5ec7c56ae2630 Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Thu, 11 Jul 2024 20:16:42 -0700 Subject: [PATCH 03/23] working(ish) copyright --- src/components/form/CustomCopyrightForm.jsx | 73 +++++++++++++-------- src/context/mintStore.ts | 9 ++- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx index 721049122..0c145fef8 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -36,24 +36,24 @@ const exclusiveRightsOptions = [ export const ClausesDescriptions = ({ clauses }) => { const descriptions = { reproduce: { - true: 'Right to Reproduction: βœ…', - false: 'Right to Reproduction: 🚫', + true: 'βœ…', + false: '🚫', }, broadcast: { - true: 'Right to Broadcast: βœ…', - false: 'Right to Broadcast: 🚫', + true: 'βœ…', + false: '🚫', }, publicDisplay: { - true: 'Right to Public Display: βœ…', - false: 'Right to Public Display: 🚫', + true: 'βœ…', + false: '🚫', }, createDerivativeWorks: { - true: 'Right to Create Derivative Works: βœ…', - false: 'Right to Create Derivative Works: 🚫', + true: 'βœ…', + false: '🚫', }, releasePublicDomain: { - true: 'Released to Public Domain: βœ…', - false: 'Released to Public Domain: 🚫', + true: 'βœ…', + false: '🚫', }, } @@ -62,17 +62,19 @@ export const ClausesDescriptions = ({ clauses }) => { Copyright Permissions Granted on Ownership:
    {Object.entries(clauses).map(([key, value]) => { + // Handle exclusive rights separately to display using the options label if (key === 'exclusiveRights') { - return ( -
  • - Exclusive Rights:{' '} - {value !== 'none' - ? `Ownership share based on ${value}` - : 'None'} -
  • - ) + const exclusiveLabel = + exclusiveRightsOptions.find((option) => option.value === value) + ?.label || 'None' + return
  • Exclusive Rights: {exclusiveLabel}
  • } - return
  • {descriptions[key][value]}
  • + // For other rights, use the descriptions dictionary + return ( +
  • + {clauseLabels[key]}: {descriptions[key][value]} +
  • + ) })}

@@ -80,10 +82,13 @@ export const ClausesDescriptions = ({ clauses }) => { ) } -function CustomCopyrightForm() { +function CustomCopyrightForm({ onChange, value }) { const { license, minterName, address } = useOutletContext() const [clauses, setClauses] = useState(initialClauses) - const [generatedDocument, setGeneratedDocument] = useState('') + const [documentText, setDocumentText] = useState('No Permissions Chosen') + const [generatedDocument, setGeneratedDocument] = useState( + 'No Permissions Chosen' + ) const updateCustomLicenseData = useMintStore( (state) => state.updateCustomLicenseData @@ -166,6 +171,7 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th return documentText }, [address, clauseNumber, clauses, minterName]) + // logic for checkboxes const handleChange = useCallback((value, name) => { const newValue = Object.prototype.hasOwnProperty.call(value, 'value') ? value.value @@ -173,7 +179,6 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th // If 'Release to Public Domain' is checked, disable and reset other clauses if (name === 'releasePublicDomain' && newValue === true) { - console.log('resetting bc public domain') setClauses({ reproduce: false, broadcast: false, @@ -183,7 +188,6 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th releasePublicDomain: true, }) } else { - console.log('setting clauses in else CCForm') setClauses((prev) => ({ ...prev, [name]: newValue, @@ -194,16 +198,24 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th } }, []) + // Logic for metadata and document updates useEffect(() => { const hasActiveRights = clauses.reproduce || clauses.broadcast || clauses.publicDisplay || - clauses.createDerivativeWorks - const documentText = generateDocumentText() + clauses.createDerivativeWorks || + clauses.releasePublicDomain - setGeneratedDocument(documentText) - updateCustomLicenseData({ clauses: clauses, documentText }) + if (!hasActiveRights) { + setGeneratedDocument('No Permissions Chosen') + setDocumentText('No Permissions Chosen') + } else { + const documentText = generateDocumentText() + setGeneratedDocument(documentText) + setDocumentText(documentText) + updateCustomLicenseData({ clauses: clauses, documentText: documentText }) + } }, [ clauses?.reproduce, clauses?.broadcast, @@ -212,8 +224,15 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th clauses?.exclusiveRights, clauses?.releasePublicDomain, handleChange, + generateDocumentText, + updateCustomLicenseData, ]) + // sync to parent State management + useEffect(() => { + onChange({ clauses, documentText }) + }, [clauses, documentText, onChange]) + return (

Custom License Generation Form

diff --git a/src/context/mintStore.ts b/src/context/mintStore.ts index 234c2ae1b..01fa92980 100644 --- a/src/context/mintStore.ts +++ b/src/context/mintStore.ts @@ -84,12 +84,11 @@ export const useMintStore = create()( isValid: false, updateCustomLicenseData: (data) => { set((state) => ({ - ...state, - customLicenseData: data // directly setting the new data + ...state, + customLicenseData: data })); - console.log("Custom license data updated in mintStore:", get().customLicenseData); - } - , + console.log("Updated customLicenseData:", get().customLicenseData); + }, reset: () => { set({ ...defaultValuesStored, ...defaultValues }) }, From 9443d379d270c0cbad7e90de55ba94dd4ec1fef0 Mon Sep 17 00:00:00 2001 From: Yannick Goossens Date: Sat, 13 Jul 2024 11:30:17 +0200 Subject: [PATCH 04/23] push license to ipfs --- src/data/ipfs.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/data/ipfs.ts b/src/data/ipfs.ts index 0bdb04cc8..ae9c04739 100644 --- a/src/data/ipfs.ts +++ b/src/data/ipfs.ts @@ -157,6 +157,7 @@ interface PrepareProps { contentRating: string formats: any } + export const prepareFile = async ({ name, description, @@ -494,7 +495,13 @@ async function buildMetadataFile({ } if (accessibility) metadata.accessibility = accessibility if (contentRating) metadata.contentRating = contentRating - if (rights === 'custom') metadata.rightUri = rightUri + if (rights === 'custom' && rightUri) { + const rightCid = await uploadFileToIPFSProxy({ + blob: new Blob([Buffer.from(JSON.stringify(rightUri))]), + path: 'license.json', + }) + metadata.rightUri = `ipfs://${rightCid}` + } if (language != null) metadata.language = language return JSON.stringify(metadata) From 11aa034d4e0a5c8428919a134c73e7ccf9eae692 Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Sat, 13 Jul 2024 15:14:10 -0700 Subject: [PATCH 05/23] Custom URI input and clauses updates --- src/atoms/input/Checkbox.jsx | 4 +- src/components/form/CustomCopyrightForm.jsx | 226 ++++++++++++++------ 2 files changed, 166 insertions(+), 64 deletions(-) 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/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx index 0c145fef8..a5aa4a5a4 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react' -import { Checkbox } from '@atoms/input' +import { Checkbox, Input } from '@atoms/input' import ReactSelect from 'react-select' import styles from '@style' import { style as select_style, theme } from '../../atoms/select/styles' @@ -13,6 +13,8 @@ const initialClauses = { createDerivativeWorks: false, exclusiveRights: 'none', // Options are 'none', 'majority', 'superMajority' releasePublicDomain: false, + customUriEnabled: false, + customUri: '', } const clauseLabels = { @@ -22,38 +24,47 @@ const clauseLabels = { createDerivativeWorks: 'Right to Create Derivative Works', exclusiveRights: 'Exclusive Rights Based on Ownership Share', releasePublicDomain: 'Release to Public Domain', + customUriEnabled: 'Custom URI', } const exclusiveRightsOptions = [ - { value: 'none', label: ' None (No Exclusive Rights To Any Party)' }, - { value: 'majority', label: ' Majority Share (50%+ Editions Owned)' }, + { 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)', + label: + ' βš–οΈ Super-Majority Share (66.667%+ Editions Owned = Exclusive Rights)', }, ] export const ClausesDescriptions = ({ clauses }) => { const descriptions = { reproduce: { - true: 'βœ…', - false: '🚫', + true: 'βœ… Yes', + false: '🚫 No', }, broadcast: { - true: 'βœ…', - false: '🚫', + true: 'βœ… Yes', + false: '🚫 No', }, publicDisplay: { - true: 'βœ…', - false: '🚫', + true: 'βœ… Yes', + false: '🚫 No', }, createDerivativeWorks: { - true: 'βœ…', - false: '🚫', + true: 'βœ… Yes', + false: '🚫 No', }, releasePublicDomain: { - true: 'βœ…', - false: '🚫', + true: 'βœ… Yes', + false: '🚫 No', + }, + customUriEnabled: { + true: 'πŸ“ Yes', + false: '🚫 No', }, } @@ -61,21 +72,31 @@ export const ClausesDescriptions = ({ clauses }) => {
Copyright Permissions Granted on Ownership:
    - {Object.entries(clauses).map(([key, value]) => { - // Handle exclusive rights separately to display using the options label - if (key === 'exclusiveRights') { - const exclusiveLabel = - exclusiveRightsOptions.find((option) => option.value === value) - ?.label || 'None' - return
  • Exclusive Rights: {exclusiveLabel}
  • - } - // For other rights, use the descriptions dictionary - return ( -
  • - {clauseLabels[key]}: {descriptions[key][value]} -
  • - ) - })} + {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 '' + } else { + const displayValue = + descriptions[key]?.[value] || 'Unknown Status' + return ( +
  • + {clauseLabels[key]}: {displayValue} +
  • + ) + } + }) + )}

@@ -85,10 +106,11 @@ export const ClausesDescriptions = ({ clauses }) => { function CustomCopyrightForm({ onChange, value }) { const { license, minterName, address } = useOutletContext() const [clauses, setClauses] = useState(initialClauses) - const [documentText, setDocumentText] = useState('No Permissions Chosen') 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 @@ -168,6 +190,12 @@ Each individual claiming ownership ("Claimant") must conclusively prove that the 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.` + return documentText }, [address, clauseNumber, clauses, minterName]) @@ -177,8 +205,28 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th ? value.value : value - // If 'Release to Public Domain' is checked, disable and reset other clauses - if (name === 'releasePublicDomain' && newValue === true) { + if (name === 'customUriEnabled') { + if (newValue) { + setClauses((prev) => ({ + ...prev, + reproduce: false, + broadcast: false, + publicDisplay: false, + createDerivativeWorks: false, + exclusiveRights: 'none', + 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, @@ -186,8 +234,11 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th createDerivativeWorks: false, exclusiveRights: 'none', releasePublicDomain: true, + customUriEnabled: false, + customUri: '', }) } else { + // Normal handling for other checkboxes setClauses((prev) => ({ ...prev, [name]: newValue, @@ -198,8 +249,31 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th } }, []) + 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') + } + } + // Logic for metadata and document updates useEffect(() => { + let documentText const hasActiveRights = clauses.reproduce || clauses.broadcast || @@ -207,15 +281,17 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th clauses.createDerivativeWorks || clauses.releasePublicDomain - if (!hasActiveRights) { - setGeneratedDocument('No Permissions Chosen') - setDocumentText('No Permissions Chosen') + if (clauses.customUriEnabled) { + documentText = `Custom URI: ${clauses.customUri}` + } else if (!hasActiveRights) { + documentText = 'No Permissions Chosen' } else { - const documentText = generateDocumentText() - setGeneratedDocument(documentText) - setDocumentText(documentText) - updateCustomLicenseData({ clauses: clauses, documentText: documentText }) + documentText = generateDocumentText() } + + setDocumentText(documentText) + setGeneratedDocument(documentText) + updateCustomLicenseData({ clauses: clauses, documentText: documentText }) }, [ clauses?.reproduce, clauses?.broadcast, @@ -233,28 +309,34 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th onChange({ clauses, documentText }) }, [clauses, documentText, onChange]) + const propertiesWithCheckboxes = [ + 'reproduce', + 'broadcast', + 'publicDisplay', + 'createDerivativeWorks', + 'releasePublicDomain', + ] + return ( -
-

Custom License Generation Form

-
- {Object.keys(clauses) - .filter((name) => name !== 'exclusiveRights') - .map((clauseName) => ( - handleChange(checked, clauseName)} - disabled={ - clauseName === 'releasePublicDomain' - ? clauses.releasePublicDomain - : false - } - /> - ))} -
- +
+

Custom License Generation Form

+
+ {propertiesWithCheckboxes.map((clauseName) => ( + handleChange(checked, clauseName)} + disabled={ + clauses.customUriEnabled || + (clauses.releasePublicDomain && + clauseName !== 'releasePublicDomain') + } + /> + ))} +
+

{clauseLabels.exclusiveRights}

value === true && value !== 'none' - ) + clauses.customUriEnabled || + !Object.values(clauses).some((value) => value && value !== 'none') } styles={select_style} theme={theme} @@ -276,6 +357,25 @@ This Agreement is entered into solely between the Creator and the Owner(s) of th classNamePrefix="react_select" />
+
+ handleChange(checked, 'customUriEnabled')} + className={styles.field} + /> + {clauses?.customUriEnabled && ( + handleUriChange(e)} + placeholder="Paste URI/URL Here (ipfs://, http://, https://)" + className={styles.field} + /> + )} + {uriError &&
{uriError}
} +
Date: Sun, 14 Jul 2024 11:10:44 -0700 Subject: [PATCH 06/23] Copyright Tab on objkt page --- src/components/form/CustomCopyrightForm.jsx | 26 +++- src/components/media-types/text/index.jsx | 1 + src/index.jsx | 2 + src/pages/objkt-display/index.tsx | 4 + src/pages/objkt-display/tabs/Copyright.jsx | 157 ++++++++++++++++++++ src/pages/objkt-display/tabs/index.js | 1 + 6 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/pages/objkt-display/tabs/Copyright.jsx diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx index a5aa4a5a4..498ae7cd0 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -12,6 +12,7 @@ const initialClauses = { 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: '', @@ -23,11 +24,12 @@ const clauseLabels = { 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', } -const exclusiveRightsOptions = [ +export const exclusiveRightsOptions = [ { value: 'none', label: ' 🚫 None (No Exclusive Rights To Any Party)' }, { value: 'majority', @@ -66,6 +68,10 @@ export const ClausesDescriptions = ({ clauses }) => { true: 'πŸ“ Yes', false: '🚫 No', }, + retainCreatorRights: { + true: 'βœ… Yes', + false: '⚠️ No', + }, } return ( @@ -175,6 +181,11 @@ No exclusive rights are granted under this Agreement. All rights are non-exclusi 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.` @@ -214,6 +225,7 @@ The rights and obligations stipulated in this Agreement, along with any associat publicDisplay: false, createDerivativeWorks: false, exclusiveRights: 'none', + retainCreatorRights: true, releasePublicDomain: false, customUriEnabled: true, })) @@ -233,6 +245,7 @@ The rights and obligations stipulated in this Agreement, along with any associat publicDisplay: false, createDerivativeWorks: false, exclusiveRights: 'none', + retainCreatorRights: true, releasePublicDomain: true, customUriEnabled: false, customUri: '', @@ -356,6 +369,17 @@ The rights and obligations stipulated in this Agreement, along with any associat 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} + /> + )}
{content} diff --git a/src/index.jsx b/src/index.jsx index 4e4480b3c..0a1caa931 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -50,6 +50,7 @@ import { History, Swap, Transfer, + Copyright, } from '@pages/objkt-display/tabs' import Display from '@pages/profile' import Collections from '@pages/profile/collections' @@ -146,6 +147,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> } /> } /> diff --git a/src/pages/objkt-display/index.tsx b/src/pages/objkt-display/index.tsx index d9dc4acaa..377db9d46 100644 --- a/src/pages/objkt-display/index.tsx +++ b/src/pages/objkt-display/index.tsx @@ -56,6 +56,10 @@ const TABS = [ to: 'transfer', private: true, }, + { + title: 'Copyright', + to: 'copyright', + } ] export const ObjktDisplay = () => { diff --git a/src/pages/objkt-display/tabs/Copyright.jsx b/src/pages/objkt-display/tabs/Copyright.jsx new file mode 100644 index 000000000..6c8f917ef --- /dev/null +++ b/src/pages/objkt-display/tabs/Copyright.jsx @@ -0,0 +1,157 @@ +import { Container } from '@atoms/layout' +import { useEffect, useState } from 'react' +import { Tags } from '@components/tags' +import styles from '@style' +import '../style.css' +import { HashToURL } from '@utils' +import { HEN_CONTRACT_FA2, LANGUAGES, LICENSE_TYPES } from '@constants' +import { getWordDate } from '@utils/time' +import { Line } from '@atoms/line' +import { useObjktDisplayContext } from '..' +import axios from 'axios' + +export const Copyright = () => { + const { nft, viewer_address } = useObjktDisplayContext() + + const [licenseData, setLicenseData] = useState(null) + + const clauseLabels = { + reproduce: 'Right to Reproduce', + broadcast: 'Right to Broadcast', + publicDisplay: 'Right to Public Display', + createDerivativeWorks: 'Right to Create Derivative Works', + exclusiveRights: 'Exclusive Rights', + retainCreatorRights: 'Retain Creator Rights Even When Exclusive', + releasePublicDomain: 'Release to Public Domain', + customUriEnabled: 'Custom URI Enabled', + customUri: 'Custom URI', + } + + 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', + }, + } + + const exclusiveRightsDescriptions = { + none: '🚫 None (No Exclusive Rights To Any Party)', + majority: 'βš–οΈ Majority Share (50%+ Editions Owned = Exclusive Rights)', + superMajority: + 'βš–οΈ Super-Majority Share (66.667%+ Editions Owned = Exclusive Rights)', + } + + useEffect(() => { + const url = HashToURL(nft?.right_uri, 'CDN', { size: 'raw' }) + console.log('url', url) + axios + .get(url) + .then((response) => { + setLicenseData(response.data) + }) + .catch((error) => console.error('Failed to fetch data:', error)) + }, [nft]) + + if (!licenseData) { + return
Loading...
+ } + + const isCustomUriOnly = () => { + return licenseData.clauses.customUriEnabled && licenseData.clauses.customUri + } + + return ( + <> + +
+

Custom License Info

+
+

URI to Agreement (Permanent)

+ + {nft.right_uri} + +
+
+

License Details

+
    + {licenseData.clauses && + Object.entries(licenseData.clauses).map(([key, value]) => { + if (isCustomUriOnly() && key !== 'customUri') { + return null // Do not render other clauses if customUriEnabled is true + } + const title = clauseLabels[key] + const displayValue = descriptions[key] + ? descriptions[key][value] + : value + if (key === 'exclusiveRights') { + return ( +
  • + Exclusive Rights: {exclusiveRightsDescriptions[value]} +
  • + ) + } + if (key === 'customUri') { + const uriDisplay = + licenseData.clauses.customUriEnabled && value ? ( + + {value} + + ) : ( + 'None' + ) + return ( +
  • + {title}: {uriDisplay} +
  • + ) + } + return ( +
  • + {title}: {displayValue} +
  • + ) + })} +
+
+
+ +
+ +

Custom License Agreement

+
+          {licenseData?.documentText}
+        
+
+ + ) +} diff --git a/src/pages/objkt-display/tabs/index.js b/src/pages/objkt-display/tabs/index.js index 1d83bc876..dcf132093 100644 --- a/src/pages/objkt-display/tabs/index.js +++ b/src/pages/objkt-display/tabs/index.js @@ -4,3 +4,4 @@ export { History } from './History' export { Info } from './Info' export { Swap } from './Swap' export { Transfer } from './Transfer' +export { Copyright } from './Copyright' From f1a7bf0128575f5aa9df7b8b8cc720f70da0f01b Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Sun, 14 Jul 2024 11:40:25 -0700 Subject: [PATCH 07/23] fixed metadata link, modified clauses for collabs --- src/components/form/CustomCopyrightForm.jsx | 4 +++- src/pages/objkt-display/tabs/Copyright.jsx | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/form/CustomCopyrightForm.jsx b/src/components/form/CustomCopyrightForm.jsx index 498ae7cd0..39914d7e6 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -129,6 +129,8 @@ function CustomCopyrightForm({ onChange, value }) { 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.` @@ -190,7 +192,7 @@ Despite reaching the threshold for exclusive rights, the Creator retains certain 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) 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.` +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.` diff --git a/src/pages/objkt-display/tabs/Copyright.jsx b/src/pages/objkt-display/tabs/Copyright.jsx index 6c8f917ef..5ede120c4 100644 --- a/src/pages/objkt-display/tabs/Copyright.jsx +++ b/src/pages/objkt-display/tabs/Copyright.jsx @@ -65,9 +65,9 @@ export const Copyright = () => { 'βš–οΈ Super-Majority Share (66.667%+ Editions Owned = Exclusive Rights)', } + const url = HashToURL(nft?.right_uri, 'CDN', { size: 'raw' }) + useEffect(() => { - const url = HashToURL(nft?.right_uri, 'CDN', { size: 'raw' }) - console.log('url', url) axios .get(url) .then((response) => { @@ -91,8 +91,8 @@ export const Copyright = () => {

Custom License Info


URI to Agreement (Permanent)

- - {nft.right_uri} + + Metadata

From b9a7a8cdc749cb1d7e2a701b6eff1ddb15cbc25f Mon Sep 17 00:00:00 2001 From: Yannick Goossens Date: Wed, 17 Jul 2024 17:13:21 +0200 Subject: [PATCH 08/23] rightUri => rightsUri --- src/context/mintStore.ts | 4 ++-- src/data/ipfs.ts | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/context/mintStore.ts b/src/context/mintStore.ts index 01fa92980..27ff0d48f 100644 --- a/src/context/mintStore.ts +++ b/src/context/mintStore.ts @@ -277,7 +277,7 @@ export const useMintStore = create()( thumbnail: used_thumb, generateDisplayUri: true, rights: license?.value, - rightUri: customLicenseData, + rightsUri: customLicenseData, language: language?.value, accessibility, contentRating, @@ -294,7 +294,7 @@ export const useMintStore = create()( cover: used_cover, thumbnail: used_thumb, rights: license?.value, - rightUri: customLicenseData, + rightsUri: customLicenseData, language: language?.value, accessibility, contentRating, diff --git a/src/data/ipfs.ts b/src/data/ipfs.ts index ae9c04739..1916ed319 100644 --- a/src/data/ipfs.ts +++ b/src/data/ipfs.ts @@ -151,7 +151,7 @@ interface PrepareProps { cover: FileForm thumbnail: FileForm rights: string - rightUri?: string + rightsUri?: string language?: string accessibility: string contentRating: string @@ -167,7 +167,7 @@ export const prepareFile = async ({ cover, thumbnail, rights, - rightUri, + rightsUri, language, accessibility, contentRating, @@ -266,7 +266,7 @@ export const prepareFile = async ({ displayUri, thumbnailUri, rights, - rightUri, + rightsUri, language, accessibility, contentRating, @@ -291,7 +291,7 @@ export const prepareDirectory = async ({ thumbnail, generateDisplayUri, rights, - rightUri, + rightsUri, language, accessibility, contentRating, @@ -306,7 +306,7 @@ export const prepareDirectory = async ({ thumbnail: FileForm generateDisplayUri: string rights: string - rightUri: string + rightsUri: string language: string accessibility: string contentRating: string @@ -388,7 +388,7 @@ export const prepareDirectory = async ({ displayUri, thumbnailUri, rights, - rightUri, + rightsUri, language, accessibility, contentRating, @@ -456,7 +456,7 @@ async function buildMetadataFile({ displayUri = '', thumbnailUri = IPFS_DEFAULT_THUMBNAIL_URI, rights, - rightUri, + rightsUri, language, accessibility, contentRating, @@ -470,7 +470,7 @@ async function buildMetadataFile({ displayUri: string thumbnailUri: string rights: string - rightUri?: string + rightsUri?: string language: string accessibility: string contentRating: string @@ -495,12 +495,12 @@ async function buildMetadataFile({ } if (accessibility) metadata.accessibility = accessibility if (contentRating) metadata.contentRating = contentRating - if (rights === 'custom' && rightUri) { - const rightCid = await uploadFileToIPFSProxy({ - blob: new Blob([Buffer.from(JSON.stringify(rightUri))]), + if (rights === 'custom' && rightsUri) { + const rightsCid = await uploadFileToIPFSProxy({ + blob: new Blob([Buffer.from(JSON.stringify(rightsUri))]), path: 'license.json', }) - metadata.rightUri = `ipfs://${rightCid}` + metadata.rightsUri = `ipfs://${rightsCid}` } if (language != null) metadata.language = language @@ -526,6 +526,6 @@ interface TeiaMetadata { //optional accessibility?: string contentRating?: string - rightUri?: string + rightsUri?: string language?: string } From a83005f3e2fa37757454e2aa2a6067c6c5bc26bc Mon Sep 17 00:00:00 2001 From: Yannick Goossens Date: Thu, 18 Jul 2024 12:41:16 +0200 Subject: [PATCH 09/23] translate ipfs:// link to ipfs gateway url --- src/pages/objkt-display/tabs/Info.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/objkt-display/tabs/Info.jsx b/src/pages/objkt-display/tabs/Info.jsx index 03567c9af..5317c6f0e 100644 --- a/src/pages/objkt-display/tabs/Info.jsx +++ b/src/pages/objkt-display/tabs/Info.jsx @@ -30,6 +30,9 @@ export const Info = () => { ? `https://anaver.se/?gallery=1&loadsingle=1&singlecontract=${HEN_CONTRACT_FA2}&singletokenid=${nft.token_id}&wallet=${viewer_address}&partnerPlatform=teia.art` : `https://anaver.se/?gallery=1&loadsingle=1&singlecontract=${HEN_CONTRACT_FA2}&singletokenid=${nft.token_id}&partnerPlatform=teia.art` const metadata_ipfs_url = HashToURL(nft.metadata_uri) + const rightsUri = nft.rights === 'custom' && nft.right_uri && nft.right_uri.startsWith('ipfs://') + ? HashToURL(nft.right_uri) + : nft.right_uri return ( <> @@ -75,7 +78,7 @@ export const Info = () => {

{nft.rights ? ( nft.rights === 'custom' ? ( - + Custom ) : ( From a6f222a35fb350eface51ca1adc2430036c70d0a Mon Sep 17 00:00:00 2001 From: ryangtanaka Date: Thu, 18 Jul 2024 14:04:48 -0700 Subject: [PATCH 10/23] copyright form v2 plus UI fixes --- 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 | 177 +++++++++++++++++--- src/components/form/copyrightmodaltext.ts | 71 ++++++++ src/components/form/index.module.scss | 13 +- src/context/mintStore.ts | 2 +- src/styles/main.scss | 16 ++ 12 files changed, 339 insertions(+), 31 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/copyrightmodaltext.ts 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 index 39914d7e6..f531e0607 100644 --- a/src/components/form/CustomCopyrightForm.jsx +++ b/src/components/form/CustomCopyrightForm.jsx @@ -1,10 +1,13 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ import React, { useState, useEffect, useCallback } from 'react' -import { Checkbox, Input } from '@atoms/input' +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, @@ -16,6 +19,7 @@ const initialClauses = { releasePublicDomain: false, customUriEnabled: false, customUri: '', + addendum: '', } const clauseLabels = { @@ -27,6 +31,7 @@ const clauseLabels = { retainCreatorRights: 'Creator Retains Rights Even When Exclusive', releasePublicDomain: 'Release to Public Domain', customUriEnabled: 'Custom URI', + overview: 'Copyright Overview', } export const exclusiveRightsOptions = [ @@ -209,6 +214,11 @@ This Agreement remains effective in perpetuity as long as the Owner(s) can concl 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]) @@ -286,6 +296,24 @@ The rights and obligations stipulated in this Agreement, along with any associat } } + // 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 @@ -332,26 +360,101 @@ The rights and obligations stipulated in this Agreement, along with any associat '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

+

+ 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') +
+ 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}

+
+

{clauseLabels.exclusiveRights}

+ handleModalOpen('exclusiveRights')} + onKeyPress={(event) => + handleKeyPress(event, clauseLabels['exclusiveRights']) + } + > + (?) + +
)}
-
- handleChange(checked, 'customUriEnabled')} - className={styles.field} - /> + {(clauses.reproduce || + clauses.broadcast || + clauses.publicDisplay || + clauses.createDerivativeWorks || + clauses.releasePublicDomain) && ( +
+

Addendum/Notes

+