Merge pull request #20 from web3privacy/dw/form

feat: form & github api
This commit is contained in:
Dominik Wane 2024-09-18 01:50:29 -07:00 committed by GitHub
commit 6a8a305ce7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 3666 additions and 126 deletions

7
app.config.ts Normal file
View File

@ -0,0 +1,7 @@
export default defineAppConfig({
github: {
appId: 0,
privateKey: '',
installationId: 0,
},
})

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
border?: boolean border?: boolean
invertedColor?: boolean
}>() }>()
</script> </script>
@ -8,14 +9,14 @@ defineProps<{
<div <div
flex flex
items-center items-center
justify-center
gap-12px gap-12px
px-12px px-12px
py-6px py-6px
cursor-pointer cursor-pointer
text-app-white :class="[border ? 'border-2px border-app-white' : 'border-0',
:class="[border ? 'border-2px border-app-white' : 'border-0']" invertedColor ? 'bg-app-white text-app-black hover:text-app-white hover:bg-app-black' : 'text-app-white bg-app-black hover:text-app-black hover:bg-app-white',
hover:text-app-black ]"
hover:bg-app-white
> >
<template v-if="!!$slots.prefix"> <template v-if="!!$slots.prefix">
<div text-24px> <div text-24px>

View File

@ -3,7 +3,7 @@ import type { InputOption } from '~/types'
const props = defineProps<{ const props = defineProps<{
options: InputOption[] options: InputOption[]
modelValue: string modelValue: string | number
count?: number count?: number
titleShowCount?: boolean titleShowCount?: boolean
}>() }>()
@ -14,7 +14,7 @@ const selectedValue = useVModel(props, 'modelValue', emits)
const isOptionSelected = computed(() => { const isOptionSelected = computed(() => {
return props.options.find(option => option.value === selectedValue.value) return props.options.find(option => option.value === selectedValue.value)
}) })
function onOptionSelected(value: string) { function onOptionSelected(value: string | number) {
emits('selected', value) emits('selected', value)
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
to: string to?: string
}>() }>()
</script> </script>

View File

@ -143,7 +143,7 @@ watch(y, (newY, oldY) => {
lg:bg-app-bg-dark_grey lg:bg-app-bg-dark_grey
> >
<div <div
v-if="!isProjectRoute" v-if="!isProjectRoute && !route.fullPath.includes('create') && !route.fullPath.includes('edit')"
relative relative
app-container app-container
w-full w-full
@ -355,7 +355,10 @@ watch(y, (newY, oldY) => {
:class="[(!showBar && width < 768) ? 'translate-y--128px' : 'translate-y-0px']" :class="[(!showBar && width < 768) ? 'translate-y--128px' : 'translate-y-0px']"
duration-200ms duration-200ms
> >
<ProjectNavigation md:mt-16px /> <ProjectNavigation
v-if="!route.fullPath.includes('create') && !route.fullPath.includes('edit')"
md:mt-16px
/>
</div> </div>
<div <div
v-else v-else

View File

@ -0,0 +1,181 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
type Token = {
name: string
symbol: string
contract_address: string
token_link: string
}
function useTokens(project?: Partial<Project>) {
const tokens = ref<Token[]>(project?.tokens as Token[] || [])
const newToken = reactive<Token>({
symbol: '',
name: '',
contract_address: '',
token_link: '',
})
function addToken() {
tokens.value.push({ ...newToken })
newToken.symbol = ''
newToken.name = ''
newToken.contract_address = ''
newToken.token_link = ''
}
function removeToken(index: number) {
tokens.value.splice(index, 1)
}
return {
tokens,
newToken,
addToken,
removeToken,
}
}
const { tokens, newToken, addToken, removeToken } = useTokens(props.project)
const assetsUsed = ref<string[]>(Array.isArray(props.project?.assets_used) ? props.project?.assets_used.map(a => a.toLowerCase()) : [])
const { assets: assetsData } = useData()
const { saveProject } = useProject()
function save() {
saveProject({
assets_used: assetsUsed.value,
have_token: tokens.value.length > 0 && tokens.value.some(token => token.contract_address),
tokens: tokens.value,
})
}
defineExpose({
save,
})
</script>
<template>
<div mt-24px>
<ClientOnly>
<ProjectCreateComponentsSelectChips
v-model="assetsUsed"
label="Assets used"
:options="assetsData?.map(asset => ({ label: asset.name, value: asset.id }))"
placeholder="Add assets"
hint="Most used assets user can use to interact with your project"
can-add-new
/>
</ClientOnly>
<ProjectCreateComponentsCategoryDivider
w-full
title="NATIVE TOKENS"
/>
<ProjectCreateComponentsItem
v-for="token in tokens"
:key="token.name"
@remove="() => removeToken(tokens.indexOf(token))"
>
<template #label>
<div
flex
gap-2px
>
<span
class="text-app-black text-14px font-700"
lg="text-16px"
> {{ token.symbol }}
</span>
<span
v-if="token.name"
class="text-app-black text-14px font-400"
lg="text-16px"
> {{ `(${token.name})` }}
</span>
</div>
</template>
<template #desc>
<NuxtLink
target="_blank"
:to="token.token_link"
hover:text-app-black
class="text-app-black/50 text-16px hidden"
lg="block"
>Etherscan
</NuxtLink>
</template>
</ProjectCreateComponentsItem>
<ProjectCreateComponentsItemAdd
button-label="ADD TOKEN"
@add="addToken()"
>
<template #content>
<div
flex
flex-col
gap-16px
lg="gap-24px"
>
<div
flex
flex-col
gap-8px
lg="flex-row gap-24px"
>
<div
class="w-100%"
flex
flex-col
gap-16px
lg="flex-row gap-24px"
>
<ProjectCreateComponentsInput
v-model="newToken.symbol"
w-full
label="Token symbol"
placeholder="Token symbol"
/>
<ProjectCreateComponentsInput
v-model="newToken.name"
w-full
label="Token name"
placeholder="Token name"
/>
</div>
<span
class="w-100%"
lg="mt-32px left-1/2 self-center"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ 'Does your project have native token? Enter Symbol, name and address' }}
</span>
</div>
<ProjectCreateComponentsInput
v-model="newToken.contract_address"
lg="w-1/2"
label="Token contract address"
placeholder="Contract address"
hint="Enter token contract address"
/>
<ProjectCreateComponentsInput
v-model="newToken.token_link"
lg="w-1/2"
label="URL for explorer"
placeholder="Token explorer URL"
hint="Link to any explorer showing data about your native token"
/>
</div>
</template>
</ProjectCreateComponentsItemAdd>
</div>
</template>

View File

@ -0,0 +1,128 @@
<script setup lang="ts">
import * as yup from 'yup'
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
const { categories: categoriesData, usecases: usecasesData, ecosystems: ecosystemsData } = useData()
const { saveProject } = useProject()
const validationSchema = yup.object().shape({
categories: yup.array().of(yup.string()).required().min(1),
usecases: yup.array().of(yup.string()).required().min(1),
ecosystems: yup.array().of(yup.string()).required().min(1),
description: yup.string().required(),
})
const { validate, meta, resetForm } = useForm({
validationSchema,
})
const { value: categories, errorMessage: categoriesError } = useField<string[]>('categories')
const { value: usecases, errorMessage: usecasesError } = useField<string[]>('usecases')
const { value: ecosystems, errorMessage: ecosystemsError } = useField<string[]>('ecosystems')
const { value: description, errorMessage: descriptionError } = useField<string>('description')
const day = ref(new Date(props.project?.product_launch_day || '').getDay())
const month = ref(new Date(props.project?.product_launch_day || '').getMonth())
const year = ref(new Date(props.project?.product_launch_day || '').getFullYear())
const isDead = ref(props.project?.sunset || false)
resetForm({
values: {
categories: Array.isArray(props.project?.categories) ? props.project?.categories?.map(c => c.toLowerCase()) : [],
usecases: Array.isArray(props.project?.usecases) ? props.project?.usecases?.map(u => u.toLowerCase()) : [],
ecosystems: Array.isArray(props.project?.ecosystem) ? props.project?.ecosystem?.map(e => e.toLowerCase()) : [],
description: props.project?.description || '',
},
})
function isFormValid() {
validate()
if (meta.value.valid) {
return true
}
else {
return false
}
}
function save() {
saveProject({
categories: categories.value,
usecases: usecases.value,
ecosystem: ecosystems.value,
description: description.value,
product_launch_day: (year.value && month.value && day.value) ? new Date(year.value, month.value, day.value).toISOString() : undefined,
sunset: isDead.value,
})
}
defineExpose({
isFormValid,
save,
})
</script>
<template>
<div mt-24px>
<ProjectCreateComponentsSelectChips
v-model="categories"
label="Categories"
required
:options="categoriesData?.map(cat => ({ label: cat.name, value: cat.id }))"
placeholder="Add category"
hint="Choose categories that fits your project"
:error="categoriesError"
/>
<ProjectCreateComponentsSelectChips
v-model="usecases"
label="Use-cases"
required
:options="usecasesData?.map(uc => ({ label: uc.name, value: uc.id }))"
placeholder="Add use-case"
hint="What can be your project used for?"
:error="usecasesError"
/>
<ProjectCreateComponentsSelectChips
v-model="ecosystems"
label="Ecosystems"
required
:options="ecosystemsData?.map(ec => ({ label: ec.name, value: ec.id }))"
placeholder="Add ecosystem"
hint="Choose ecosystems that is your project part of"
:error="ecosystemsError"
/>
<ProjectCreateComponentsInput
v-model="description"
lg="w-1/2"
label="Description"
text-area
required
hint="What kind of technology you use, what are your special features
and why should user use your project."
placeholder="Write something about your project"
:error="descriptionError"
/>
<ProjectCreateComponentsDatePicker
v-model:day="day"
v-model:month="month"
v-model:year="year"
label="Project launch date"
hint="Date of project emergence (Optional)"
/>
<span
text-16px
font-400
lg="text-16px"
>Other Information
</span>
<ProjectCreateComponentsToggle
v-model="isDead"
label="Sunset (project is dead)"
hint="Check if project is currently running and working"
/>
</div>
</template>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
type Fundings = {
name: string
link: string
}
function useFundings(project?: Partial<Project>) {
const fundings = ref<Fundings[]>(project?.funding as Fundings[] || [])
const newFunding = reactive<Fundings>({
name: '',
link: '',
})
function addFunding() {
fundings.value.push({ ...newFunding })
newFunding.name = ''
newFunding.link = ''
}
function removeFunding(index: number) {
fundings.value.splice(index, 1)
}
return {
fundings,
newFunding,
addFunding,
removeFunding,
}
}
const { fundings, newFunding, addFunding, removeFunding } = useFundings(props.project)
const { saveProject } = useProject()
function save() {
saveProject({
funding: fundings.value,
})
}
defineExpose({
save,
})
</script>
<template>
<div>
<ProjectCreateComponentsCategoryDivider
w-full
title="FUNDING"
/>
<ProjectCreateComponentsItem
v-for="funding in fundings"
:key="funding.name"
@remove="() => removeFunding(fundings.indexOf(funding))"
>
<template #label>
<div
flex
gap-2px
>
<span
class="text-app-black text-14px font-700"
lg="text-16px"
> {{ funding.name }}
</span>
</div>
</template>
<template #desc>
<NuxtLink
:to="funding.link"
hover:text-app-black
class="text-app-black/50 text-16px hidden"
lg="block"
>Link
</NuxtLink>
</template>
</ProjectCreateComponentsItem>
<ProjectCreateComponentsItemAdd
button-label="ADD FUNDING"
@add="addFunding()"
>
<template #content>
<div
flex
flex-col
gap-16px
lg="gap-24px"
>
<ProjectCreateComponentsInput
v-model="newFunding.name"
lg="w-50%"
label="Name"
placeholder="Investor's name"
hint="Name of entity investing into project"
/>
<ProjectCreateComponentsInput
v-model="newFunding.link"
lg="w-50%"
label="Link confirming investment"
placeholder="Add URL"
hint="Link to Article, PR or other sources confirming funding information"
/>
</div>
</template>
</ProjectCreateComponentsItemAdd>
</div>
</template>

View File

@ -0,0 +1,165 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
type Event = {
title: string
description: string
link: string
time: string
}
function useEvents(project?: Partial<Project>) {
const events = ref<Event[]>(project?.history as Event[] || [])
const newEvent = reactive<Event>({
title: '',
description: '',
link: '',
time: '',
})
const day = ref<number>()
const month = ref<number>()
const year = ref<number>()
function addEvent() {
events.value.push({ ...newEvent })
newEvent.title = ''
newEvent.description = ''
newEvent.link = ''
newEvent.time = ''
day.value = undefined
month.value = undefined
year.value = undefined
}
function removeEvent(index: number) {
events.value.splice(index, 1)
}
return {
events,
newEvent,
day,
month,
year,
addEvent,
removeEvent,
}
}
const { events, newEvent, addEvent, removeEvent, day, month, year } = useEvents(props.project)
watch([day, month, year], () => {
const yearValue = year.value
const monthValue = month.value
const dayValue = day.value
if (!yearValue || !monthValue || !dayValue) {
return null
}
newEvent.time = new Date(yearValue, monthValue - 1, dayValue).toString()
})
function formatDate(date: string) {
const d = new Date(date)
const day = String(d.getDate()).padStart(2, '0')
const month = String(d.getMonth() + 1).padStart(2, '0')
const year = d.getFullYear()
return `${day}/${month}/${year}`
}
const { saveProject } = useProject()
function save() {
saveProject({
history: events.value,
})
}
defineExpose({
save,
})
</script>
<template>
<div mt-24px>
<ProjectCreateComponentsItem
v-for="event in events"
:key="event.title"
@remove="() => removeEvent(events.indexOf(event))"
>
<template #label>
<div
flex
gap-2px
>
<span
class="text-app-black text-14px font-700"
lg="text-16px"
> {{ event.title }}
</span>
</div>
</template>
<template #desc>
<span
class="text-app-black/50 text-16px hidden"
lg="block"
>{{ formatDate(event.time) }}
</span>
<NuxtLink
:to="event.link"
hover:text-app-black
class="text-app-black/50 text-16px hidden"
lg="block"
>Link
</NuxtLink>
</template>
</ProjectCreateComponentsItem>
<ProjectCreateComponentsItemAdd
button-label="ADD EVENT"
@add="addEvent()"
>
<template #content>
<div
flex
flex-col
gap-16px
lg="gap-24px"
>
<ProjectCreateComponentsInput
v-model="newEvent.title"
lg="w-50%"
label="Event title"
placeholder="Enter title of event"
hint="Specific name of events (or what happened)"
/>
<ProjectCreateComponentsInput
v-model="newEvent.description"
lg="w-50%"
label="Event description"
placeholder="Describe this event"
hint="Detailed description of event (max. 1000 glyphs)"
/>
<ProjectCreateComponentsInput
v-model="newEvent.link"
lg="w-50%"
label="Event URL"
placeholder="Enter URL with event details"
hint="Link to Article, PR or other sources confirming event information"
/>
<ProjectCreateComponentsDatePicker
v-model:day="day"
v-model:month="month"
v-model:year="year"
label="Date of event"
placeholder="When this event happened?"
/>
</div>
</template>
</ProjectCreateComponentsItemAdd>
</div>
</template>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
const web = ref<string>(props.project?.links?.web || '')
const blog = ref<string>(props.project?.links?.blog || '')
const github = ref<string>(props.project?.links?.github || '')
const forum = ref<string>(props.project?.links?.forum || '')
const docs = ref<string>(props.project?.links?.docs || '')
const whitepaper = ref<string>(props.project?.links?.whitepaper || '')
const block_explorer = ref<string>(props.project?.links?.block_explorer || '')
const governance = ref<string>(props.project?.links?.governance || '')
const twitter = ref<string>(props.project?.links?.twitter || '')
const discord = ref<string>(props.project?.links?.discord || '')
const telegram = ref<string>(props.project?.links?.telegram || '')
const lens = ref<string>(props.project?.links?.lens || '')
const farcaster = ref<string>(props.project?.links?.farcaster || '')
const { saveProject } = useProject()
function save() {
saveProject({
links: {
web: web.value,
blog: blog.value,
github: github.value,
forum: forum.value,
docs: docs.value,
whitepaper: whitepaper.value,
block_explorer: block_explorer.value,
governance: governance.value,
twitter: twitter.value,
discord: discord.value,
telegram: telegram.value,
lens: lens.value,
farcaster: farcaster.value,
},
})
}
defineExpose({
save,
})
</script>
<template>
<div mt-24px>
<div
flex
flex-col
gap-16px
lg="grid grid-cols-2 gap-24px"
>
<ProjectCreateComponentsInput
v-model="web"
w-full
label="Website"
placeholder="ex. https://www.yourproject.com"
/>
<ProjectCreateComponentsInput
v-model="blog"
w-full
label="Blog"
placeholder="https://blog,yourproject.com"
/>
<ProjectCreateComponentsInput
v-model="github"
w-full
label="Github repository"
placeholder="Github.com/Yourproject"
/>
<ProjectCreateComponentsInput
v-model="forum"
w-full
label="Forum"
placeholder="https://forum,yourproject.com"
/>
</div>
<ProjectCreateComponentsCategoryDivider
w-full
title="TECHNOGY DETAILS"
/>
<div
flex
flex-col
gap-16px
lg="grid grid-cols-2 gap-24px"
>
<ProjectCreateComponentsInput
v-model="docs"
w-full
label="Documentation"
placeholder="https://docs.yourproject.com"
/>
<ProjectCreateComponentsInput
v-model="whitepaper"
w-full
label="Whitepaper"
placeholder="https://www.yourproject.com/whitepaper.pdf"
/>
<ProjectCreateComponentsInput
v-model="block_explorer"
w-full
label="Explorer"
placeholder="https://explorer.yourproject.com"
/>
<ProjectCreateComponentsInput
v-model="governance"
w-full
label="Governance"
placeholder="https://governance.yourproject.com"
/>
</div>
<ProjectCreateComponentsCategoryDivider
w-full
title="SOCIAL NETWORKS"
/>
<div
flex
flex-col
gap-16px
lg="grid grid-cols-2 gap-24px"
>
<ProjectCreateComponentsInput
v-model="twitter"
w-full
label="Twitter"
placeholder="Twitter profile URL"
/>
<ProjectCreateComponentsInput
v-model="discord"
w-full
label="Discord"
placeholder="Discord invite link"
/>
<ProjectCreateComponentsInput
v-model="telegram"
w-full
label="Telegram"
placeholder="Telegram channel URL"
/>
<ProjectCreateComponentsInput
v-model="lens"
w-full
label="Lens"
placeholder="Lens profile URL"
/>
<ProjectCreateComponentsInput
v-model="farcaster"
w-full
label="Farcaster"
placeholder="Farcaster profile URL"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,104 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
const privacyPolicyUrl = ref(props.project?.privacy_policy?.link || '')
const compliance = ref(props.project?.compliance || '')
const kyc = ref(props.project?.tracebility?.kyc || false)
const defaultPrivacy = ref(props.project?.default_privacy || false)
const signRequirements = ref(Array.isArray(props.project?.tracebility?.sign_in_type_requirments) ? props.project?.tracebility?.sign_in_type_requirments?.map(s => s.toLowerCase()) : [])
const trackedData = ref(props.project?.tracebility?.tracked_data || '')
const dataUsage = ref(props.project?.privacy_policy?.data_usage || '')
const { signInRequirments: signInRequirmentsData } = useData()
const { saveProject } = useProject()
function save() {
saveProject({
privacy_policy: {
defined: privacyPolicyUrl.value ? true : false,
link: privacyPolicyUrl.value,
data_usage: dataUsage.value,
},
compliance: compliance.value,
tracebility: {
kyc: kyc.value,
tracked_data: trackedData.value,
},
default_privacy: defaultPrivacy.value,
})
}
defineExpose({
save,
})
</script>
<template>
<div mt-24px>
<div
flex
flex-col
gap-24px
>
<ProjectCreateComponentsInput
v-model="privacyPolicyUrl"
lg="w-50%"
label="Link to projects Privacy Policy"
placeholder="https://policy.yourproject.com"
hint="URL of document defining you privacy policy and data usage"
/>
<ProjectCreateComponentsInput
v-model="compliance"
lg="w-50%"
label="Compliance"
placeholder="Enter compliance that project follow"
hint="Is project Compliant with regulations? (ex. OFAC, Privacy Act)"
/>
<ProjectCreateComponentsToggle
v-model="kyc"
label="KYC (Know your customer)"
hint="Is project requiering KYC for its users?"
/>
<ProjectCreateComponentsToggle
v-model="defaultPrivacy"
label="Defult privacy"
hint="Is maximum privacy turned on by default,"
/>
</div>
<ProjectCreateComponentsCategoryDivider
w-full
title="ADDITIONAL INFO"
/>
<div
flex
flex-col
gap-24px
>
<ProjectCreateComponentsSelectChips
v-model="signRequirements"
label="Sign-in requirements"
:options="signInRequirmentsData.map(s => ({ label: s.name, value: s.id }))"
placeholder="Add requirement"
hint="What do you need to provide to use your project?"
/>
<ProjectCreateComponentsInput
v-model="trackedData"
lg="w-50%"
label="Tracked data"
placeholder="Enter tracked data"
hint="What data does project collect? (ex. IP, Location, E-mail, Wallet,...)"
/>
<ProjectCreateComponentsInput
v-model="dataUsage"
lg="w-50%"
label="Data usage"
placeholder="What are you using data for?"
hint="How does project use user info (resseling, information gathering, none,...)"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,198 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
type Audit = {
name: string
url: string
time: string
}
function useAudits(project?: Partial<Project>) {
const audits = ref<Audit[]>(project?.audits as Audit[] || [])
const newAudit = reactive<Audit>({
name: '',
url: '',
time: '',
})
const day = ref<number>()
const month = ref<number>()
const year = ref<number>()
function addAudit() {
audits.value.push({ ...newAudit })
newAudit.name = ''
newAudit.url = ''
newAudit.time = ''
day.value = undefined
month.value = undefined
year.value = undefined
}
function removeAudit(index: number) {
audits.value.splice(index, 1)
}
return {
audits,
newAudit,
day,
month,
year,
addAudit,
removeAudit,
}
}
const { audits, newAudit, addAudit, removeAudit, day, month, year } = useAudits(props.project)
watch([day, month, year], () => {
const yearValue = year.value
const monthValue = month.value
const dayValue = day.value
if (!yearValue || !monthValue || !dayValue) {
return null
}
newAudit.time = new Date(yearValue, monthValue - 1, dayValue).toString()
})
function formatDate(date: string) {
const d = new Date(date)
const day = String(d.getDate()).padStart(2, '0')
const month = String(d.getMonth() + 1).padStart(2, '0')
const year = d.getFullYear()
return `${day}/${month}/${year}`
}
const thirdPartyDep = ref(props.project?.third_party_dependency || '')
const socialTrust = ref(props.project?.social_trust || '')
const spof = ref(props.project?.technical_spof || '')
const { saveProject } = useProject()
function save() {
saveProject({
audits: audits.value,
third_party_dependency: thirdPartyDep.value,
social_trust: socialTrust.value,
technical_spof: spof.value,
})
}
defineExpose({
save,
})
</script>
<template>
<div>
<ProjectCreateComponentsCategoryDivider
w-full
title="AUDITS"
/>
<ProjectCreateComponentsItem
v-for="audit in audits"
:key="audit.name"
@remove="() => removeAudit(audits.indexOf(audit))"
>
<template #label>
<div
flex
gap-2px
>
<span
class="text-app-black text-14px font-700"
lg="text-16px"
> {{ audit.name }}
</span>
</div>
</template>
<template #desc>
<span
class="text-app-black/50 text-16px hidden"
lg="block"
>{{ formatDate(audit.time) }}
</span>
<NuxtLink
target="_blank"
:to="audit.url"
hover:text-app-black
class="text-app-black/50 text-16px hidden"
lg="block"
>Link
</NuxtLink>
</template>
</ProjectCreateComponentsItem>
<ProjectCreateComponentsItemAdd
button-label="ADD AUDIT"
@add="addAudit()"
>
<template #content>
<div
flex
flex-col
gap-16px
lg="gap-24px"
>
<ProjectCreateComponentsInput
v-model="newAudit.name"
lg="w-50%"
label="Audit name"
placeholder="Audit name"
hint="Title and Name of audit company"
/>
<ProjectCreateComponentsInput
v-model="newAudit.url"
lg="w-50%"
label="URL of audit"
placeholder="URL of audit"
hint="Enter URL of audit"
/>
<ProjectCreateComponentsDatePicker
v-model:day="day"
v-model:month="month"
v-model:year="year"
label="Date of audit"
placeholder="When has been project audited?"
/>
</div>
</template>
</ProjectCreateComponentsItemAdd>
<ProjectCreateComponentsCategoryDivider
w-full
title="ADDITIONAL INFO"
/>
<ProjectCreateComponentsInput
v-model="thirdPartyDep"
textarea
:textarea-rows="3"
lg="w-50%"
label="Third party dependency"
placeholder="Write about dependencies"
hint="Is your project dependend on third service like Uniswap pools, Network or third party contract, Chainlink oracle,...?"
/>
<ProjectCreateComponentsInput
v-model="socialTrust"
textarea
:textarea-rows="3"
lg="w-50%"
label="Social trust"
placeholder="Who does project trust with treasury and security?"
hint="Who is governing project and how? DAO, Multisig wallet of X,"
/>
<ProjectCreateComponentsInput
v-model="spof"
textarea
:textarea-rows="3"
lg="w-50%"
label="Single point of failure"
placeholder="What is single point of failture for project"
hint="What have to happen to shutdown, hack project or leak data? ex. Hack of SHA256, Stolen admin keys,..."
/>
</div>
</template>

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
const isAnonymousTeam = ref<boolean>(props.project?.team?.anonymous || false)
type Member = {
name: string
link: string
}
function useMembers(project?: Partial<Project>) {
const members = ref<Member[]>(project?.team?.teammembers as Member[] || [])
const newMember = reactive<Member>({
name: '',
link: '',
})
function addMember() {
members.value.push({ ...newMember })
newMember.name = ''
newMember.link = ''
}
function removeMember(index: number) {
members.value.splice(index, 1)
}
return {
members,
newMember,
addMember,
removeMember,
}
}
const { members, newMember, addMember, removeMember } = useMembers(props.project)
const { saveProject } = useProject()
function save() {
saveProject({
team: {
anonymous: isAnonymousTeam.value,
teammembers: members.value,
},
})
}
defineExpose({
save,
})
</script>
<template>
<div mt-24px>
<ProjectCreateComponentsToggle
v-model="isAnonymousTeam"
label="Anonymous team"
hint="Are developers able to upgrade and change deployed contract?"
/>
<ProjectCreateComponentsCategoryDivider
w-full
title="TEAM MEMBERS"
/>
<ProjectCreateComponentsItem
v-for="member in members"
:key="member.name"
@remove="() => removeMember(members.indexOf(member))"
>
<template #label>
<div
flex
gap-2px
>
<span
class="text-app-black text-14px font-700"
lg="text-16px"
> {{ member.name }}
</span>
</div>
</template>
<template #desc>
<NuxtLink
target="_blank"
:to="member.link"
hover:text-app-black
class="text-app-black/50 text-16px hidden"
lg="block"
>Link
</NuxtLink>
</template>
</ProjectCreateComponentsItem>
<ProjectCreateComponentsItemAdd
button-label="ADD MEMBER"
@add="addMember()"
>
<template #content>
<div
flex
flex-col
gap-16px
lg="gap-24px"
>
<ProjectCreateComponentsInput
v-model="newMember.name"
lg="w-50%"
label="Team member name"
placeholder="Enter member name"
hint="Full member's name"
/>
<ProjectCreateComponentsInput
v-model="newMember.link"
lg="w-50%"
label="Profile URL"
placeholder="Linkedin, Farcaster, Twitter,..."
hint="Link to members social profile (ex. Twitter, Linkedin, Lens,...)"
/>
</div>
</template>
</ProjectCreateComponentsItemAdd>
</div>
</template>

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import type { Project } from '~/types'
const props = defineProps<{
project?: Partial<Project>
}>()
const version = ref(props.project?.project_phase?.toLowerCase() || undefined)
const openSource = ref(props.project?.blockchain_features?.opensource || false)
const upgradability = ref(props.project?.blockchain_features?.upgradability?.enabled || false)
const assetType = ref(props.project?.blockchain_features?.asset_custody_type?.toLowerCase() || '')
const encryption = ref(props.project?.blockchain_features?.encryption || '')
const peerToPeer = ref(props.project?.blockchain_features?.p2p)
const decentralizedStorage = ref(props.project?.storage?.decentralized)
const { projectPhase: projectPhaseData, assetCustody: assetCustodyData } = useData()
const { saveProject } = useProject()
function save() {
saveProject({
project_phase: version.value,
blockchain_features: {
opensource: openSource.value,
upgradability: {
enabled: upgradability.value,
},
asset_custody_type: assetType.value,
encryption: encryption.value,
p2p: peerToPeer.value,
},
storage: {
decentralized: decentralizedStorage.value,
},
})
}
defineExpose({
save,
})
</script>
<template>
<div>
<ProjectCreateComponentsCategoryDivider
w-full
title="VERSION"
/>
<ProjectCreateComponentsRadio
v-model="version"
:options="projectPhaseData.map(p => ({ label: p.name, value: p.id }))"
/>
<ProjectCreateComponentsCategoryDivider
w-full
title="FEATURES"
/>
<div
flex
flex-col
gap-24px
>
<ProjectCreateComponentsToggle
v-model="openSource"
label="Open source"
hint="Check when projects source code is openly available and editable"
/>
<ProjectCreateComponentsToggle
v-model="upgradability"
label="Upgradability"
hint="Are developers able to upgrade and change deployed contract?"
/>
</div>
<ProjectCreateComponentsSelect
v-model="assetType"
:options="assetCustodyData.map(c => ({ label: c.name, value: c.id }))"
label="Asset custody type"
placeholder="Select custody type"
hint="How are users fund handled? (non-custody, multisig, pool,...)"
/>
<ProjectCreateComponentsCategoryDivider
w-full
title="ADDITIONAL INFO"
/>
<div
flex
flex-col
gap-24px
>
<ProjectCreateComponentsInput
v-model="encryption"
lg="w-1/2"
label="Technology type / Encryption"
hint="Define technologies your project uses for privacy ex. Zero-Knowledge (ZK), SHA256,"
placeholder="Technology name"
/>
<ProjectCreateComponentsToggle
v-model="peerToPeer"
label="Peer to Peer (P2P)"
hint="Check when you transfer / communicate withou intermediaries"
/>
<ProjectCreateComponentsToggle
v-model="decentralizedStorage"
label="Decentralized storage"
hint="Is your data hosted on IPFS, Filecoin or other decentralized storage?"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
<template>
<div
w-full
flex
justify-between
gap-8px
my-40px
lg="gap-32px"
>
<div
flex
items-center
gap-8px
text-16px
font-700
class="text-app-white/50"
>
<span text-nowrap>{{ title }}</span>
</div>
<div
w-full
flex
items-center
>
<hr
border="t-2px white/50"
w-full
shrink-1
>
</div>
</div>
</template>

View File

@ -0,0 +1,119 @@
<script setup lang="ts">
export interface SelectProps {
label?: string
hint?: string
placeholder?: string
required?: boolean
modelValue?: any
}
defineProps<SelectProps>()
const months = [
{ label: 'January', value: 1 },
{ label: 'February', value: 2 },
{ label: 'March', value: 3 },
{ label: 'April', value: 4 },
{ label: 'May', value: 5 },
{ label: 'June', value: 6 },
{ label: 'July', value: 7 },
{ label: 'August', value: 8 },
{ label: 'September', value: 9 },
{ label: 'October', value: 10 },
{ label: 'November', value: 11 },
{ label: 'December', value: 12 },
]
const years = Array.from({ length: 101 }, (_, i) => new Date().getFullYear() - i)
const day = defineModel<number>('day')
const month = defineModel<number>('month')
const year = defineModel<number>('year')
const daysInMonth = (month: number, year: number) => {
const monthObj = months.find(m => m.value === month)
if (!monthObj) {
throw new Error(`Invalid month value: ${month}`)
}
const monthIndex = months.indexOf(monthObj)
return new Date(year, monthIndex + 1, 0).getDate()
}
const days = computed(() => {
if (month.value && year.value) {
return Array.from({ length: daysInMonth(month.value, year.value) }, (_, i) => i + 1)
}
return Array.from({ length: 31 }, (_, i) => i + 1)
})
</script>
<template>
<div
flex
flex-col
gap-8px
>
<label
v-if="label"
font-400
text-14px
lg:text-16px
text-app-white
>
{{ label }}
<span
v-if="required"
text-app-danger
text-16px
>*</span>
</label>
<div
class="flex flex-col"
gap-8px
lg="flex flex-row gap-24px"
>
<div
class="flex flex-row"
lg="w-1/2"
relative
>
<SelectBox
v-model="day"
w-full
:options="days.map(day => ({ label: day.toString(), value: day }))"
placeholder="Day"
:border-opacity="30"
:is-margin-top="false"
/>
<SelectBox
v-model="month"
w-full
mx--2px
:options="months.map(month => ({ label: month.label, value: month.value }))"
placeholder="Month"
:border-opacity="30"
:is-margin-top="false"
/>
<SelectBox
v-model="year"
w-full
:options="years.map(year => ({ label: year.toString(), value: year }))"
placeholder="Year"
:border-opacity="30"
:is-margin-top="false"
/>
</div>
<span
v-if="hint"
lg="left-1/2 self-center"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ hint }}
</span>
</div>
</div>
</template>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
export interface SelectProps {
label?: string
hint?: string
placeholder?: string
required?: boolean
modelValue?: any
textarea?: boolean
textareaRows?: number
lgWidth?: string
error?: string
}
defineProps<SelectProps>()
const model = defineModel<string>()
</script>
<template>
<div
class="w-full!"
flex
flex-col
gap-8px
>
<label
v-if="label"
font-400
text-14px
lg:text-16px
text-app-white
>
{{ label }}
<span
v-if="required"
text-app-danger
text-16px
>*</span>
</label>
<div
w-full
flex
flex-col
gap-8px
lg="flex flex-row gap-24px"
relative
>
<div v-bind="$attrs">
<textarea
v-if="textarea"
v-model="model"
:rows="textareaRows"
:placeholder="placeholder"
class="relative w-full p-8px text-left border-2px text-app-white bg-black sm:text-sm sm:leading-6 focus:ring-0 focus:outline-none"
:class="error ? 'border-app-danger/50' : 'border-white/30'"
/>
<input
v-else
v-model="model"
:placeholder="placeholder"
type="text"
class="relative w-full p-8px text-left border-2px text-app-white bg-black sm:text-sm sm:leading-6 focus:ring-0 focus:outline-none"
:class="error ? 'border-app-danger/50' : 'border-white/30'"
>
</div>
<span
v-if="hint"
lg="left-1/2 self-center"
v-bind="$attrs"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ hint }}
</span>
</div>
</div>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
const emits = defineEmits(['remove'])
</script>
<template>
<div
flex
justify-between
items-center
py-12px
px-16px
bg-app-white
>
<slot name="label" />
<div
flex
items-center
gap-16px
>
<slot
class="text-app-black/50"
name="desc"
/>
<button @click="emits('remove')">
<UnoIcon
i-heroicons-solid-x
text-12px
text-app-black
/>
</button>
</div>
</div>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
defineProps<{
buttonLabel: string
}>()
const emits = defineEmits(['add'])
</script>
<template>
<div
flex
flex-col
w-full
border-2
border-app-white
p-24px
>
<slot name="content" />
<Button
mt-16px
lg="w-fit mt-24px"
border
inverted-color
@click="emits('add')"
>
<span
px-24px
>{{ buttonLabel }}</span>
</Button>
</div>
</template>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
interface OptionItem<T> {
label: string
value: T
}
export interface SelectProps {
options: OptionItem<string>[]
}
defineProps<SelectProps>()
const selected = defineModel<string>()
</script>
<template>
<div w-full>
<div w-full>
<HeadlessRadioGroup v-model="selected">
<div class="flex flex-wrap gap-20px">
<HeadlessRadioGroupOption
v-for="option in options"
:key="option.label"
v-slot="{ checked }"
as="template"
:value="option.value"
>
<div class="cursor-pointer w-49% flex items-center gap-22px">
<div
rounded-full
p-6px
flex
items-center
justify-centerě
:class="checked ? 'bg-app-white text-app-black' : 'outline outline-2px outline-offset--2 outline-app-white text-app-white'"
>
<UnoIcon
v-if="checked"
i-heroicons-solid-check
text-20px
/>
<div
v-else
w-20px
h-20px
/>
</div>
<HeadlessRadioGroupLabel
as="p"
:class="checked ? 'text-app-white' : 'text-app-white/50'"
class="font-medium"
>
{{ option.label }}
</HeadlessRadioGroupLabel>
</div>
</HeadlessRadioGroupOption>
</div>
</HeadlessRadioGroup>
</div>
</div>
</template>

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import type { InputOption } from '~/types'
const props = defineProps<SelectProps>()
const emits = defineEmits(['update:modelValue'])
interface SelectProps {
options: InputOption[]
label?: string
modelValue: string
placeholder?: string
required?: boolean
hint?: string
}
const selectedValue = useVModel(props, 'modelValue', emits)
</script>
<template>
<div
flex
flex-col
gap-8px
lg="flex flex-row gap-24px"
relative
>
<div lg="w-1/2">
<HeadlessListbox
v-model="selectedValue"
as="div"
>
<HeadlessListboxLabel
v-if="label"
font-400
text-14px
lg:text-16px
text-app-white
>
{{ label }}
<span
v-if="required"
text-app-danger
text-16px
>*</span>
</HeadlessListboxLabel>
<div
class="relative font-700 mt-2 bg-app-black"
>
<HeadlessListboxButton
as="div"
class="relative w-full cursor-pointer p-8px text-left border-2px text-app-white bg-black border-white/30 sm:text-sm sm:leading-6"
>
<span
class="block truncate mr-8px"
:class="[selectedValue ? 'text-app-white' : 'font-400 text-white/50']"
>
{{ props.options.find(option => option.value === selectedValue)?.label || props.placeholder }}
</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2">
<UnoIcon
i-heroicons-solid-chevron-down
class="text-app-white"
aria-hidden="true"
/>
</span>
</HeadlessListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<HeadlessListboxOptions
class="absolute z-100 max-h-60 w-full divide-y-2px border-2px border-t-0 overflow-auto bg-app-black text-app-white focus:outline-none sm:text-sm border-white/30"
>
<HeadlessListboxOption
v-for="option in props.options"
:key="option.value"
v-slot="{ selected }"
as="template"
:value="option.value"
class="w-full relative cursor-pointer select-none py-8px p-16px border-white/30"
>
<span
class="block truncate"
:class="[selected ? 'font-semibold' : 'font-normal']"
>{{ option.label }}</span>
</HeadlessListboxOption>
</HeadlessListboxOptions>
</transition>
</div>
</HeadlessListbox>
</div>
<span
v-if="hint"
lg="mt-28px left-1/2 self-center"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ hint }}
</span>
</div>
</template>

View File

@ -0,0 +1,213 @@
<script setup lang="ts">
interface OptionItem<T> {
label: string
value: T
}
export interface SelectProps {
options: OptionItem<any>[]
label?: string
hint?: string
placeholder?: string
required?: boolean
multiple?: boolean
canAddNew?: boolean
modelValue?: any
error?: string
}
const props = withDefaults(defineProps<SelectProps>(), {
multiple: true,
})
const emits = defineEmits(['update:modelValue'])
const modelValue = useVModel(props, 'modelValue', emits)
const query = ref('')
const newOptions = ref<OptionItem<any>[]>([])
const options = computed(() => {
return [...props.options, ...newOptions.value]
})
const filteredOptions = computed(() =>
query.value === ''
? options.value
: options.value.filter(o => o.label.toLowerCase().includes(query.value.toLowerCase())),
)
// const modelValue = ref(props.modelValue || (props.multiple ? [] : null))
const selectedOptions = computed(() => props.multiple ? options.value.filter(o => modelValue.value?.includes(o.value)) : [])
function deleteOption(value: string) {
const index = modelValue.value.indexOf(value)
if (index !== -1)
modelValue.value.splice(index, 1)
newOptions.value = newOptions.value.filter((o: any) => o.value !== value)
}
function addOption() {
if (query.value === '' || !props.canAddNew) return
if (!options.value.some(o => o.value === query.value)) {
newOptions.value.push({ label: query.value, value: query.value })
}
if (props.multiple) {
if (!modelValue.value) modelValue.value = []
modelValue.value.push(query.value)
}
else {
modelValue.value = query.value
}
query.value = ''
}
</script>
<template>
<div
flex
flex-col
gap-8px
lg="flex flex-row gap-24px"
relative
>
<div lg="w-1/2">
<HeadlessCombobox
v-model="modelValue"
:multiple="props.multiple"
>
<HeadlessComboboxLabel
v-if="label"
font-400
text-14px
lg:text-16px
text-app-white
>
{{ label }}
<span
v-if="required"
text-app-danger
text-16px
>*</span>
</HeadlessComboboxLabel>
<div class="relative font-700 mt-2 bg-app-black">
<HeadlessComboboxButton
as="div"
class="relative w-full cursor-pointer p-8px text-left border-2px text-app-white bg-black sm:text-sm sm:leading-6"
:class="error ? 'border-app-danger/50' : 'border-white/30'"
>
<div class="flex flex-wrap gap-8px">
<span
v-for="option in selectedOptions"
:key="option.value"
class="font-700 text-14px leading-20px flex gap-8px items-center bg-app-white text-black px-8px py-4px"
lg="text-16px"
>
{{ option.label }}
<button
type="button"
class="ml-1 text-black"
@click.stop="deleteOption(option.value)"
>
<UnoIcon
i-heroicons-solid-x
text-16px
text-black
/>
</button>
</span>
<HeadlessComboboxInput
class="text-14px font-400 leading-20px ml-8px w-fit bg-transparent border-none focus:ring-0 focus:outline-none"
lg="16px"
:placeholder="placeholder"
@keyup.enter="addOption"
@change="query = $event.target.value"
/>
</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-2">
<UnoIcon
i-heroicons-solid-chevron-down
class="text-app-white"
aria-hidden="true"
/>
</span>
</HeadlessComboboxButton>
</div>
<Transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
@after-leave="query = ''"
>
<HeadlessComboboxOptions
lg="w-1/2"
class="w-full absolute z-100 max-h-60 divide-y-2px border-2px border-t-0 overflow-auto bg-app-black text-app-white focus:outline-none sm:text-sm border-white/30"
>
<div
v-if="filteredOptions.length === 0 && query !== '' && props.canAddNew"
class="gap-4px flex items-center relative cursor-default select-none px-4 py-2 text-app-white"
@click="addOption"
>
<UnoIcon
i-heroicons-solid-plus
class="text-app-white"
/>
<span>Add</span>
</div>
<div
v-else-if="filteredOptions.length === 0 && query !== '' && !props.canAddNew"
class="relative cursor-default select-none px-4 py-2 text-app-white opacity-50"
>
Nothing found.
</div>
<HeadlessComboboxOption
v-for="option in filteredOptions"
:key="option.value"
v-slot="{ selected, active }"
class="border-white/30"
as="template"
:value="option.value"
>
<li
class="w-full relative cursor-pointer select-none py-8px p-16px"
:class="[active ? 'bg-#ffffff1a' : 'text-white']"
>
<span
class="block truncate"
:class="[selected ? 'font-semibold' : 'font-normal']"
>{{ option.label }}</span>
<span
v-if="selected"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-white"
>
<UnoIcon
i-heroicons-solid-check
text-20px
aria-hidden="true"
/>
</span>
</li>
</HeadlessComboboxOption>
</HeadlessComboboxOptions>
</Transition>
</HeadlessCombobox>
</div>
<span
v-if="hint"
lg="mt-28px left-1/2 self-center"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ hint }}
</span>
</div>
</template>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
defineProps<{
label?: string
hint?: string
}>()
const enabled = defineModel<boolean>()
</script>
<template>
<div
flex
flex-col
gap-8px
lg="flex flex-row gap-24px"
relative
>
<div
flex
items-center
gap-24px
lg="w-1/2"
>
<HeadlessSwitch
v-model="enabled"
:class="enabled ? 'bg-white/10' : 'bg-white/10'"
class="relative inline-flex h-[28px] w-[68px] shrink-0 cursor-pointer transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75"
>
<span
aria-hidden="true"
:class="enabled ? 'translate-x-9 bg-app-white' : 'translate-x-0 bg-app-black border-2 border-app-white'"
flex
items-center
class="pointer-events-none inline-block h-[28px] w-[32px] transform shadow-lg ring-0 transition duration-200 ease-in-out"
>
<UnoIcon
v-if="enabled"
transition
duration-200
ease-in-out
i-heroicons-solid-check
w-full
text-20px
text-app-black
/>
<UnoIcon
v-else
transition
duration-200
ease-in-out
i-heroicons-solid-x
w-full
text-20px
text-app-white
/>
</span>
</HeadlessSwitch>
<span
v-if="label"
text-14px
font-400
lg="text-16px"
:class="enabled ? 'text-app-white' : 'text-app-white/50'"
>
{{ label }}
</span>
</div>
<span
v-if="hint"
lg="left-1/2 self-center"
font-400
italic
lg:text-14px
text-12px
text-app-white
opacity-50
>
{{ hint }}
</span>
</div>
</template>

View File

@ -23,11 +23,12 @@ defineProps<{
> >
<div mt-32px> <div mt-32px>
<ProjectActivityLaunch <ProjectActivityLaunch
v-if="project.history" v-for="launch in project.history"
:network="project.history?.title" :key="launch.title"
:date="project.history?.time" :network="launch.title"
:launch-type="project.history?.description" :date="launch.time"
:to="project.history?.link" :launch-type="launch.description"
:to="launch.link"
/> />
</div> </div>
<div <div

View File

@ -25,12 +25,12 @@ defineProps<{
gap-y-16px gap-y-16px
lg:grid-cols-4 lg:grid-cols-4
> >
<ProjectInfoItem <!-- <ProjectInfoItem
:check-undefined="project.history?.time" :check-undefined="project.history?.time"
title="Date of creation" title="Date of creation"
> >
{{ formatDate(project.history!.time!) }} {{ formatDate(project.history!.time!) }}
</ProjectInfoItem> </ProjectInfoItem> -->
</div> </div>
</ProjectDetailContainer> </ProjectDetailContainer>
</template> </template>

View File

@ -3,9 +3,9 @@ import { useRoute } from 'vue-router'
const route = useRoute() const route = useRoute()
const githubProjectUrl = computed(() => { // const githubProjectUrl = computed(() => {
return `https://github.com/web3privacy/explorer-data/blob/main/src/projects/${route.params.id}/index.yaml` // return `https://github.com/web3privacy/explorer-data/blob/main/src/projects/${route.params.id}/index.yaml`
}) // })
</script> </script>
<template> <template>
@ -20,7 +20,7 @@ const githubProjectUrl = computed(() => {
> >
<NavigationButton <NavigationButton
w-230px w-230px
@click="$router.back()" @click="$router.push('/')"
> >
<span <span
block block
@ -39,9 +39,8 @@ const githubProjectUrl = computed(() => {
<EditButton <EditButton
px-16px px-16px
py-8px py-8px
hover:bg-white hover="cursor-pointer bg-white text-black"
hover:text-black @click="$router.push('/project/' + route.params.id + '/edit')"
:to="githubProjectUrl"
> >
<span <span
text-16px text-16px

View File

@ -301,8 +301,8 @@ defineProps<{
</div> </div>
</div> </div>
<ProjectOpenessTeamMembers <ProjectOpenessTeamMembers
v-if="project?.team" v-if="project?.team?.teammembers"
:members="project.team" :members="project.team?.teammembers"
mt-24px mt-24px
/> />
<div mt-32px> <div mt-32px>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
members: { members: {
name: string name?: string
role?: string role?: string
link?: string link?: string
}[] | undefined }[] | undefined

View File

@ -39,7 +39,7 @@ defineProps<{
:color="project.blockchain_features?.asset_custody_type === 'non-custody' ? '#18FF2F' : project.blockchain_features?.asset_custody_type === 'multisig' ? '#FFB800' : '#FF0000'" :color="project.blockchain_features?.asset_custody_type === 'non-custody' ? '#18FF2F' : project.blockchain_features?.asset_custody_type === 'multisig' ? '#FFB800' : '#FF0000'"
bold bold
> >
{{ project.blockchain_features?.asset_custody_type.toUpperCase() }} {{ project.blockchain_features?.asset_custody_type?.toUpperCase() }}
</ProjectInfoItem> </ProjectInfoItem>
<ProjectInfoItem <ProjectInfoItem
title="Upgradability" title="Upgradability"

View File

@ -4,14 +4,18 @@ import type { InputOption } from '~/types'
const props = withDefaults(defineProps<SelectProps>(), { const props = withDefaults(defineProps<SelectProps>(), {
isMarginTop: true, isMarginTop: true,
blackAndWhite: true, blackAndWhite: true,
borderOpacity: 100,
}) })
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
interface SelectProps { interface SelectProps {
options: InputOption[] options: InputOption[]
modelValue: string label?: string
modelValue: any
isMarginTop?: boolean isMarginTop?: boolean
blackAndWhite?: boolean blackAndWhite?: boolean
borderOpacity?: number
placeholder?: string
} }
const selectedValue = useVModel(props, 'modelValue', emits) const selectedValue = useVModel(props, 'modelValue', emits)
@ -27,10 +31,15 @@ const selectedValue = useVModel(props, 'modelValue', emits)
:class="[isMarginTop ? 'mt-2' : 'mt-0', blackAndWhite ? 'bg-app-black' : 'bg-app-white']" :class="[isMarginTop ? 'mt-2' : 'mt-0', blackAndWhite ? 'bg-app-black' : 'bg-app-white']"
> >
<HeadlessListboxButton <HeadlessListboxButton
class="relative w-full cursor-pointer py-8px p-16px text-left border-2px sm:text-sm sm:leading-6" class="relative w-full cursor-pointer py-8px p-16px text-left border-2px sm:text-sm sm:leading-6"
:class="[blackAndWhite ? ' text-app-white' : 'text-app-black']" :class="[blackAndWhite ? 'text-app-white' : 'text-app-black', `border-white/${borderOpacity}`]"
> >
<span class="block truncate mr-8px">{{ props.options.find(option => option.value === selectedValue)?.label }}</span> <span
class="block truncate mr-8px"
:class="[selectedValue ? 'text-app-white' : 'font-400 text-white/50']"
>
{{ props.options.find(option => option.value === selectedValue)?.label || props.placeholder }}
</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<UnoIcon <UnoIcon
i-ic-baseline-arrow-drop-down i-ic-baseline-arrow-drop-down
@ -45,24 +54,22 @@ const selectedValue = useVModel(props, 'modelValue', emits)
leave-to-class="opacity-0" leave-to-class="opacity-0"
> >
<HeadlessListboxOptions <HeadlessListboxOptions
:class="`border-white/${borderOpacity}`"
class="absolute z-100 max-h-60 w-full divide-y-2px border-2px border-t-0 overflow-auto bg-app-black text-app-white focus:outline-none sm:text-sm" class="absolute z-100 max-h-60 w-full divide-y-2px border-2px border-t-0 overflow-auto bg-app-black text-app-white focus:outline-none sm:text-sm"
> >
<HeadlessListboxOption <HeadlessListboxOption
v-for="option in props.options" v-for="option in props.options"
:key="option.value" :key="option.value"
v-slot="{ active, selected }" v-slot="{ selected }"
as="template" as="template"
:value="option.value" :value="option.value"
class="py-8px p-16px cursor-pointer"
:class="`border-white/${borderOpacity}`"
> >
<li <span
class="w-full relative cursor-pointer select-none py-8px p-16px" class="block truncate"
:class="[active ? 'bg-#ffffff1a' : 'text-white']" :class="[selected ? 'font-semibold' : 'font-normal']"
> >{{ option.label }}</span>
<span
class="block truncate"
:class="[selected ? 'font-semibold' : 'font-normal']"
>{{ option.label }}</span>
</li>
</HeadlessListboxOption> </HeadlessListboxOption>
</HeadlessListboxOptions> </HeadlessListboxOptions>
</transition> </transition>

View File

@ -3,7 +3,7 @@ import type { InputOption } from '~/types'
const props = defineProps<{ const props = defineProps<{
options: InputOption[] options: InputOption[]
modelValue: string modelValue: string | number
}>() }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])

View File

@ -6,6 +6,10 @@ import type { Rank } from '~/types/rank'
import type { Usecase } from '~/types/usecase' import type { Usecase } from '~/types/usecase'
export const useData = defineStore('data', () => { export const useData = defineStore('data', () => {
const projectPhase = useState<{ id: string, name: string }[]>('projectPhase')
const assetCustody = useState<{ id: string, name: string }[]>('assetCustody')
const signInRequirments = useState<{ id: string, name: string }[]>('signInRequirmenets')
const categories = useState<Category[]>('categories') const categories = useState<Category[]>('categories')
const usecases = useState<Usecase[]>('usecases') const usecases = useState<Usecase[]>('usecases')
const features = useState<Feature[]>('features') const features = useState<Feature[]>('features')
@ -47,6 +51,9 @@ export const useData = defineStore('data', () => {
const data = await $fetch<{ const data = await $fetch<{
categories: Category[] categories: Category[]
projects: Project[] projects: Project[]
project_phase: { id: string, name: string }[]
asset_custody_type: { id: string, name: string }[]
sign_in_type_requirments: { id: string, name: string }[]
usecases: Usecase[] usecases: Usecase[]
ecosystems: Ecosystem[] ecosystems: Ecosystem[]
assets: Asset[] assets: Asset[]
@ -57,17 +64,17 @@ export const useData = defineStore('data', () => {
...project, ...project,
ratings: generateProjectRating(project), ratings: generateProjectRating(project),
})).filter(p => p.name) })).filter(p => p.name)
categories.value = data.categories.map((c) => {
c.projectsCount = projects.value.filter(p => categories.value = data.categories
p.categories?.includes(c.id),
).length
return c
}).filter(c => c.projectsCount > 0)
usecases.value = data.usecases usecases.value = data.usecases
ecosystems.value = data.ecosystems ecosystems.value = data.ecosystems
assets.value = data.assets assets.value = data.assets
features.value = data.features features.value = data.features
ranks.value = data.ranks ranks.value = data.ranks
projectPhase.value = data.project_phase.map(p => ({ id: p.id.toLowerCase(), name: p.name }))
assetCustody.value = data.asset_custody_type.map(a => ({ id: a.id.toLowerCase(), name: a.name }))
signInRequirments.value = data.sign_in_type_requirments.map(s => ({ id: s.id.toLowerCase(), name: s.name }))
} }
catch (e) { catch (e) {
console.error(e) console.error(e)
@ -267,6 +274,9 @@ export const useData = defineStore('data', () => {
filter, filter,
switcher, switcher,
categories, categories,
projectPhase,
assetCustody,
signInRequirments,
usecases, usecases,
features, features,
ecosystems, ecosystems,

71
composables/useProject.ts Normal file
View File

@ -0,0 +1,71 @@
import { Buffer } from 'buffer'
import type { Project } from '~/types'
export const useProject = defineStore('project', () => {
const project = ref<Partial<Project>>()
const projectImage = ref<File>()
const isPublishing = ref(false)
const { getProjectById } = useData()
function setProject(id: string) {
project.value = getProjectById(id, { shallow: false }) as Project
}
function clearProject() {
project.value = undefined
}
function saveProject(data: Partial<Project>) {
project.value = {
...project.value,
...data,
}
}
function saveProjectImage(image: File) {
projectImage.value = image
}
async function publishProject() {
isPublishing.value = true
try {
const imageArrayBuffer = await projectImage.value?.arrayBuffer()
let imageBuffer: Buffer | undefined
let base64Image: string | undefined
if (imageArrayBuffer) {
imageBuffer = Buffer.from(imageArrayBuffer)
const base64String = imageBuffer.toString('base64')
base64Image = base64String
}
await $fetch(`/api/data`, {
method: 'POST',
body: {
project: project.value,
image: {
type: projectImage.value?.type,
data: base64Image,
},
},
})
}
catch (e) {
console.error(e)
}
finally {
isPublishing.value = false
}
}
return {
project,
isPublishing,
setProject,
clearProject,
saveProject,
saveProjectImage,
publishProject,
}
})

6
layouts/create.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<div h-full w-full>
<Navigation />
<slot />
</div>
</template>

View File

@ -10,6 +10,7 @@ export default defineNuxtConfig({
'nuxt-lodash', 'nuxt-lodash',
'nuxt-headlessui', 'nuxt-headlessui',
'@nuxt/image', '@nuxt/image',
'@vee-validate/nuxt',
], ],
sourcemap: { sourcemap: {
server: true, server: true,

View File

@ -20,6 +20,7 @@
"@iconify-json/bi": "^1.2.0", "@iconify-json/bi": "^1.2.0",
"@iconify-json/heroicons-outline": "^1.2.0", "@iconify-json/heroicons-outline": "^1.2.0",
"@iconify-json/heroicons-solid": "^1.2.0", "@iconify-json/heroicons-solid": "^1.2.0",
"@iconify-json/eos-icons": "^1.1.10",
"@iconify-json/ic": "^1.2.0", "@iconify-json/ic": "^1.2.0",
"@iconify-json/iconoir": "^1.2.0", "@iconify-json/iconoir": "^1.2.0",
"@iconify-json/material-symbols": "^1.2.1", "@iconify-json/material-symbols": "^1.2.1",
@ -32,16 +33,20 @@
"@nuxtjs/color-mode": "^3.4.4", "@nuxtjs/color-mode": "^3.4.4",
"@pinia/nuxt": "^0.5.4", "@pinia/nuxt": "^0.5.4",
"@unocss/nuxt": "^0.62.3", "@unocss/nuxt": "^0.62.3",
"@vee-validate/nuxt": "^4.13.2",
"@vueuse/nuxt": "^11.0.3", "@vueuse/nuxt": "^11.0.3",
"eslint": "^9.9.1", "eslint": "^9.9.1",
"nuxt": "^3.13.0", "nuxt": "^3.13.0",
"nuxt-headlessui": "^1.2.0", "nuxt-headlessui": "^1.2.0",
"nuxt-lodash": "^2.5.3", "nuxt-lodash": "^2.5.3",
"octokit": "^4.0.2",
"pinia": "^2.2.2", "pinia": "^2.2.2",
"taze": "^0.16.7", "taze": "^0.16.7",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vitest": "^2.0.5", "vitest": "^2.0.5",
"vue-tsc": "^2.1.4" "vue-tsc": "^2.1.4",
"yaml": "^2.5.1",
"yup": "^1.4.0"
}, },
"overrides": { "overrides": {
"vue": "latest" "vue": "latest"

421
pages/project/[id]/edit.vue Normal file
View File

@ -0,0 +1,421 @@
<script setup lang="ts">
import * as yup from 'yup'
import ProjectCreateCategoriesBasicInfo from '~/components/Project/Create/Categories/BasicInfo.vue'
import ProjectCreateCategoriesAssets from '~/components/Project/Create/Categories/Assets.vue'
import ProjectCreateCategoriesLinks from '~/components/Project/Create/Categories/Links.vue'
import ProjectCreateCategoriesTechnology from '~/components/Project/Create/Categories/Technology.vue'
import ProjectCreateCategoriesPrivacy from '~/components/Project/Create/Categories/Privacy.vue'
import ProjectCreateCategoriesSecurity from '~/components/Project/Create/Categories/Security.vue'
import ProjectCreateCategoriesTeam from '~/components/Project/Create/Categories/Team.vue'
import ProjectCreateCategoriesFunding from '~/components/Project/Create/Categories/Funding.vue'
import ProjectCreateCategoriesHistory from '~/components/Project/Create/Categories/History.vue'
definePageMeta({
layout: 'create',
})
const { projects } = useData()
const { saveProject, setProject, publishProject, saveProjectImage } = useProject()
const { project, isPublishing } = storeToRefs(useProject())
const route = useRoute()
await until(projects).toMatch(p => p?.length > 0)
setProject(route.params.id as string)
if (!project.value) {
throw createError({
statusCode: 404,
message: 'Project not found',
fatal: true,
})
}
const tabs = reactive([
{ label: 'Basic Info', value: 'basic_info', component: markRaw(ProjectCreateCategoriesBasicInfo) },
{ label: 'Assets', value: 'assets', component: markRaw(ProjectCreateCategoriesAssets) },
{ label: 'Links', value: 'links', component: markRaw(ProjectCreateCategoriesLinks) },
{ label: 'Technology', value: 'technology', component: markRaw(ProjectCreateCategoriesTechnology) },
{ label: 'Privacy', value: 'privacy', component: markRaw(ProjectCreateCategoriesPrivacy) },
{ label: 'Security', value: 'security', component: markRaw(ProjectCreateCategoriesSecurity) },
{ label: 'Team', value: 'team', component: markRaw(ProjectCreateCategoriesTeam) },
{ label: 'Funding', value: 'funding', component: markRaw(ProjectCreateCategoriesFunding) },
{ label: 'History', value: 'history', component: markRaw(ProjectCreateCategoriesHistory) },
])
const selectedTab = ref(tabs[0].value)
function getCurrentComponent() {
const tab = tabs.find(t => t.value === selectedTab.value) || tabs[0]
return tab.component || null
}
const currentComponent = ref()
const { open, onChange } = useFileDialog({
accept: 'image/*', // Set to accept only image files
})
const logoSrc = ref(project.value?.logos?.[0].url || '/no-image-1-1.svg')
onChange((files) => {
if (!files?.[0]) return
const file = files[0]
const reader = new FileReader()
reader.onload = (e) => {
logoSrc.value = e.target?.result as string
}
reader.readAsDataURL(file)
saveProjectImage(file)
})
const projectNameInput = ref<HTMLInputElement | null>(null)
function useProjectName() {
const isEditing = ref(false)
// const name = ref('Untitled')
const { value: name, errorMessage: nameError } = useField<string>('name', yup.string().required().notOneOf(['Untitled', 'Undefined', 'Create', 'create']))
name.value = project.value?.name || 'Untitled'
function toggleEdit() {
isEditing.value = !isEditing.value
if (isEditing.value) {
setTimeout(() => {
projectNameInput.value?.focus()
}, 0)
}
}
return {
isEditing,
name,
nameError,
toggleEdit,
}
}
const { isEditing, name, nameError, toggleEdit } = useProjectName()
name.value = project.value?.name || 'Untitled'
function save() {
saveProject({
name: name.value,
})
}
async function next() {
if (nameError.value)
return
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
else save()
}
currentComponent.value.save()
const currentIndex = tabs.findIndex(tab => tab.value === selectedTab.value)
const nextTab = tabs[currentIndex + 1] || tabs[0]
selectedTab.value = nextTab.value
}
async function publish() {
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
}
else if (isPublishing) {
return
}
await publishProject()
navigateTo(`/project/${project.value?.id || project.value?.name?.toLowerCase().replace(/\s+/g, '-')}`)
}
function jumpTo(tab: string) {
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
else save()
}
currentComponent.value.save()
selectedTab.value = tab
}
const transitionDone = ref(false)
</script>
<template>
<div w-full>
<div
bg-app-bg-dark_grey
px-16px
py-24px
lg="pb-0px mb--1px"
>
<div app-container>
<div
flex
items-center
gap-16px
>
<div
relative
class="parent"
>
<NuxtImg
lg="w-100px h-100px"
w-64px
h-64px
bg-app-bg-grey
object-cover
border-2
class="border-app-white/30"
:src="logoSrc ?? '/no-image-1-1.svg'"
/>
<button
h-24px
hidden
parent-hover:flex
absolute
bottom-0
w-full
border-t-0
border-2
border-black
border-opacity-50
h-fit
justify-center
text="12px"
font-700
bg-app-white
text-app-black
@click="open()"
>
Upload Logo
</button>
</div>
<div
flex
flex-col
gap-8px
>
<h3
font-400
text="14px app-white/50"
leading-20px
>
{{ 'Project Name' }}
</h3>
<div
flex
items-center
gap-12px
relative
>
<input
v-if="isEditing"
ref="projectNameInput"
v-model="name"
w-fit
onkeydown="this.style.width = 0; this.style.width = this.scrollWidth + 2 + 'px';"
type="text"
font-700
text-20px
leading-28px
bg-app-bg-dark_grey
onfocus="this.style.width = 0; this.style.width = this.scrollWidth + 2 + 'px';"
>
<h2
v-else
font-700
text-20px
leading-28px
>
{{ name }}
</h2>
<button @click="toggleEdit()">
<UnoIcon
v-if="isEditing"
text-24px
class="text-app-white/30"
hover:text-app-white
i-heroicons-solid-check
/>
<UnoIcon
v-else
text-20px
class="text-app-white/30"
hover:text-app-white
i-heroicons-solid-pencil
/>
</button>
<span
v-if="nameError"
text-nowrap
text-app-danger
text-12px
absolute
lg:bottom--24px
bottom--16px
select-none
>Invalid project name</span>
</div>
</div>
</div>
<div
flex
w-full
gap-46px
lg="mt-24px"
>
<SelectBox
:model-value="selectedTab"
label="Choose category"
:options="tabs.map(t => ({ label: t.label, value: t.value }))"
:border-opacity="30"
w-full
lg:hidden
block
mt-16px
@update:model-value="$event => jumpTo($event)"
/>
<button
v-for="tab in tabs"
:key="tab.value"
lg:block
hidden
pb-8px
leading-40px
:class="selectedTab === tab.value ? 'font-bold border-b-4 border-app-white' : ''"
@click="jumpTo(tab.value)"
>
{{ tab.label }}
</button>
</div>
</div>
</div>
<div
border-t-2
class="border-app-white/30"
px-16px
py-24px
>
<div
app-container
mb-170px
lg="mb-55px"
>
<ClientOnly>
<Transition
v-if="!transitionDone"
name="fade"
mode="out-in"
appear
@after-enter="transitionDone = true"
>
<component
:is="getCurrentComponent()"
ref="currentComponent"
:project="project"
w-full
flex
flex-col
gap-24px
/>
</Transition>
<component
:is="getCurrentComponent()"
ref="currentComponent"
:project="project"
w-full
flex
flex-col
gap-24px
/>
<Button
v-if="selectedTab !== tabs[tabs.length - 1].value"
class="hidden!"
mt-48px
lg="w-fit flex!"
border
@click="next()"
>
<span px-24px>NEXT SECTION</span>
</Button>
</ClientOnly>
</div>
</div>
<div
flex
flex-col
gap-16px
justify-center
text-center
fixed
bottom-0
w-full
bg-app-bg-dark_grey
class="border-app-white/30"
lg="bg-app-black w-fit border-l-2 border-t-2 right-0 border-app-white"
p-12px
>
<Button
v-if="selectedTab !== tabs[tabs.length - 1].value"
flex
lg="w-fit hidden!"
border
@click="next()"
>
<span px-24px>NEXT SECTION</span>
</Button>
<span
v-if="selectedTab !== tabs[tabs.length - 1].value"
lg="hidden"
block
text="12px italic app-white/50"
>or you can submit changes by publishing them</span>
<div flex>
<Button
w-full
lg="w-fit"
border
@click="navigateTo(`/project/${route.params.id}`)"
>
<span px-24px>CANCEL</span>
</Button>
<Button
w-full
lg="w-fit"
inverted-color
@click="publish()"
>
<UnoIcon
v-if="isPublishing"
w-108px
i-eos-icons-loading
text-black
text-18px
/>
<span
v-else
px-24px
>PUBLISH</span>
</Button>
</div>
</div>
</div>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

407
pages/project/create.vue Normal file
View File

@ -0,0 +1,407 @@
<script setup lang="ts">
import * as yup from 'yup'
import ProjectCreateCategoriesBasicInfo from '~/components/Project/Create/Categories/BasicInfo.vue'
import ProjectCreateCategoriesAssets from '~/components/Project/Create/Categories/Assets.vue'
import ProjectCreateCategoriesLinks from '~/components/Project/Create/Categories/Links.vue'
import ProjectCreateCategoriesTechnology from '~/components/Project/Create/Categories/Technology.vue'
import ProjectCreateCategoriesPrivacy from '~/components/Project/Create/Categories/Privacy.vue'
import ProjectCreateCategoriesSecurity from '~/components/Project/Create/Categories/Security.vue'
import ProjectCreateCategoriesTeam from '~/components/Project/Create/Categories/Team.vue'
import ProjectCreateCategoriesFunding from '~/components/Project/Create/Categories/Funding.vue'
import ProjectCreateCategoriesHistory from '~/components/Project/Create/Categories/History.vue'
definePageMeta({
layout: 'create',
})
const { saveProject, publishProject, saveProjectImage } = useProject()
const { project, isPublishing } = storeToRefs(useProject())
const tabs = reactive([
{ label: 'Basic Info', value: 'basic_info', component: markRaw(ProjectCreateCategoriesBasicInfo) },
{ label: 'Assets', value: 'assets', component: markRaw(ProjectCreateCategoriesAssets) },
{ label: 'Links', value: 'links', component: markRaw(ProjectCreateCategoriesLinks) },
{ label: 'Technology', value: 'technology', component: markRaw(ProjectCreateCategoriesTechnology) },
{ label: 'Privacy', value: 'privacy', component: markRaw(ProjectCreateCategoriesPrivacy) },
{ label: 'Security', value: 'security', component: markRaw(ProjectCreateCategoriesSecurity) },
{ label: 'Team', value: 'team', component: markRaw(ProjectCreateCategoriesTeam) },
{ label: 'Funding', value: 'funding', component: markRaw(ProjectCreateCategoriesFunding) },
{ label: 'History', value: 'history', component: markRaw(ProjectCreateCategoriesHistory) },
])
const selectedTab = ref(tabs[0].value)
function getCurrentComponent() {
const tab = tabs.find(t => t.value === selectedTab.value) || tabs[0]
return tab.component || null
}
const currentComponent = ref()
const { open, onChange } = useFileDialog({
accept: 'image/*', // Set to accept only image files
})
const logoSrc = ref('/no-image-1-1.svg')
onChange((files) => {
if (!files?.[0]) return
const file = files[0]
const reader = new FileReader()
reader.onload = (e) => {
logoSrc.value = e.target?.result as string
}
reader.readAsDataURL(file)
saveProjectImage(file)
})
const projectNameInput = ref<HTMLInputElement | null>(null)
function useProjectName() {
const isEditing = ref(false)
// const name = ref('Untitled')
const { value: name, errorMessage: nameError } = useField<string>('name', yup.string().required().notOneOf(['Untitled', 'Undefined', 'Create', 'create']))
name.value = project.value?.name || 'Untitled'
function toggleEdit() {
isEditing.value = !isEditing.value
if (isEditing.value) {
setTimeout(() => {
projectNameInput.value?.focus()
}, 0)
}
}
return {
isEditing,
name,
nameError,
toggleEdit,
}
}
const { isEditing, name, nameError, toggleEdit } = useProjectName()
function save() {
saveProject({
...project.value,
name: name.value,
})
}
function next() {
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
else save()
}
currentComponent.value.save()
const currentIndex = tabs.findIndex(tab => tab.value === selectedTab.value)
const nextTab = tabs[currentIndex + 1] || tabs[0]
selectedTab.value = nextTab.value
}
async function publish() {
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
}
else if (isPublishing) {
return
}
await publishProject()
navigateTo(`/project/${project.value?.id || project.value?.name?.toLowerCase().replace(/\s+/g, '-')}`)
}
function jumpTo(tab: string) {
if (selectedTab.value === 'basic_info') {
if (!currentComponent.value.isFormValid())
return
else save()
}
currentComponent.value.save()
selectedTab.value = tab
}
const transitionDone = ref(false)
</script>
<template>
<div w-full>
<div
bg-app-bg-dark_grey
px-16px
py-24px
lg="pb-0px mb--1px"
>
<div app-container>
<div
flex
items-center
gap-16px
>
<div
relative
class="parent"
>
<NuxtImg
lg="w-100px h-100px"
w-64px
h-64px
bg-app-bg-grey
object-cover
border-2
class="border-app-white/30"
opacity-30
:src="logoSrc ?? '/no-image-1-1.svg'"
/>
<button
h-24px
hidden
parent-hover:flex
absolute
bottom-0
w-full
border-t-0
border-2
border-black
border-opacity-80
h-fit
justify-center
text="12px"
font-700
bg-app-white
text-app-black
@click="open()"
>
Upload Logo
</button>
</div>
<div
flex
flex-col
gap-8px
>
<h3
font-400
text="14px app-white/50"
leading-20px
>
{{ 'Project Name' }}
</h3>
<div
flex
items-center
gap-12px
relative
>
<input
v-if="isEditing"
ref="projectNameInput"
v-model="name"
w-fit
onkeydown="this.style.width = 0; this.style.width = this.scrollWidth + 2 + 'px';"
type="text"
font-700
text-20px
leading-28px
bg-app-bg-dark_grey
onfocus="this.style.width = 0; this.style.width = this.scrollWidth + 2 + 'px';"
>
<h2
v-else
font-700
text-20px
leading-28px
>
{{ name }}
</h2>
<button @click="toggleEdit()">
<UnoIcon
v-if="isEditing"
text-24px
class="text-app-white/30"
hover:text-app-white
i-heroicons-solid-check
/>
<UnoIcon
v-else
text-20px
class="text-app-white/30"
hover:text-app-white
i-heroicons-solid-pencil
/>
</button>
<span
v-if="nameError"
text-nowrap
text-app-danger
text-12px
absolute
lg:bottom--24px
bottom--16px
select-none
>Invalid project name</span>
</div>
</div>
</div>
<div
flex
w-full
gap-46px
lg="mt-24px"
>
<SelectBox
:model-value="selectedTab"
label="Choose category"
:options="tabs.map(t => ({ label: t.label, value: t.value }))"
:border-opacity="30"
w-full
lg:hidden
block
mt-16px
@update:model-value="$event => jumpTo($event)"
/>
<button
v-for="tab in tabs"
:key="tab.value"
lg:block
hidden
pb-8px
leading-40px
:class="selectedTab === tab.value ? 'font-bold border-b-4 border-app-white' : ''"
@click="selectedTab = tab.value"
>
{{ tab.label }}
</button>
</div>
</div>
</div>
<div
border-t-2
class="border-app-white/30"
px-16px
py-24px
>
<div
app-container
mb-170px
lg="mb-55px"
>
<ClientOnly>
<Transition
v-if="!transitionDone"
name="fade"
mode="out-in"
appear
@after-enter="transitionDone = true"
>
<component
:is="getCurrentComponent()"
ref="currentComponent"
:project="project"
w-full
flex
flex-col
gap-24px
/>
</Transition>
<component
:is="getCurrentComponent()"
v-else
ref="currentComponent"
:project="project"
w-full
flex
flex-col
gap-24px
/>
<Button
v-if="selectedTab !== tabs[tabs.length - 1].value"
class="hidden!"
mt-48px
lg="w-fit flex!"
border
@click="next()"
>
<span px-24px>NEXT SECTION</span>
</Button>
</ClientOnly>
</div>
</div>
<div
flex
flex-col
gap-16px
justify-center
text-center
fixed
bottom-0
w-full
bg-app-bg-dark_grey
class="border-app-white/30"
lg="bg-app-black w-fit border-l-2 border-t-2 right-0 border-app-white"
p-12px
>
<Button
v-if="selectedTab !== tabs[tabs.length - 1].value"
flex
lg="w-fit hidden!"
border
@click="next()"
>
<span px-24px>NEXT SECTION</span>
</Button>
<span
v-if="selectedTab !== tabs[tabs.length - 1].value"
lg="hidden"
block
text="12px italic app-white/50"
>or you can submit changes by publishing them</span>
<div flex>
<Button
w-full
lg="w-fit"
border
@click="navigateTo('/')"
>
<span px-24px>CANCEL</span>
</Button>
<Button
w-full
lg="w-fit"
inverted-color
@click="publish()"
>
<UnoIcon
v-if="isPublishing"
w-108px
i-eos-icons-loading
text-black
text-18px
/>
<span
v-else
px-24px
>PUBLISH</span>
</Button>
</div>
</div>
</div>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -18,6 +18,9 @@ importers:
'@iconify-json/bi': '@iconify-json/bi':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
'@iconify-json/eos-icons':
specifier: ^1.1.10
version: 1.2.0
'@iconify-json/heroicons-outline': '@iconify-json/heroicons-outline':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
@ -29,7 +32,7 @@ importers:
version: 1.2.0 version: 1.2.0
'@iconify-json/iconoir': '@iconify-json/iconoir':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.1
'@iconify-json/material-symbols': '@iconify-json/material-symbols':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@ -38,7 +41,7 @@ importers:
version: 1.2.0 version: 1.2.0
'@iconify-json/simple-icons': '@iconify-json/simple-icons':
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2 version: 1.2.3
'@nuxt/devtools': '@nuxt/devtools':
specifier: ^1.4.1 specifier: ^1.4.1
version: 1.4.1(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0)) version: 1.4.1(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))
@ -60,6 +63,9 @@ importers:
'@unocss/nuxt': '@unocss/nuxt':
specifier: ^0.62.3 specifier: ^0.62.3
version: 0.62.3(magicast@0.3.5)(postcss@8.4.44)(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(webpack@5.89.0(esbuild@0.23.1)) version: 0.62.3(magicast@0.3.5)(postcss@8.4.44)(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(webpack@5.89.0(esbuild@0.23.1))
'@vee-validate/nuxt':
specifier: ^4.13.2
version: 4.13.2(magicast@0.3.5)(rollup@4.21.2)(vue@3.4.38(typescript@5.5.4))
'@vueuse/nuxt': '@vueuse/nuxt':
specifier: ^11.0.3 specifier: ^11.0.3
version: 11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@20.8.7)(encoding@0.1.13)(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@4.21.2)(terser@5.22.0)(typescript@5.5.4)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(vue-tsc@2.1.4(typescript@5.5.4)))(rollup@4.21.2)(vue@3.4.38(typescript@5.5.4)) version: 11.0.3(magicast@0.3.5)(nuxt@3.13.0(@parcel/watcher@2.4.1)(@types/node@20.8.7)(encoding@0.1.13)(eslint@9.9.1(jiti@1.21.6))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.3)(rollup@4.21.2)(terser@5.22.0)(typescript@5.5.4)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0))(vue-tsc@2.1.4(typescript@5.5.4)))(rollup@4.21.2)(vue@3.4.38(typescript@5.5.4))
@ -75,6 +81,9 @@ importers:
nuxt-lodash: nuxt-lodash:
specifier: ^2.5.3 specifier: ^2.5.3
version: 2.5.3(rollup@4.21.2) version: 2.5.3(rollup@4.21.2)
octokit:
specifier: ^4.0.2
version: 4.0.2
pinia: pinia:
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.2.2(typescript@5.5.4)(vue@3.4.38(typescript@5.5.4)) version: 2.2.2(typescript@5.5.4)(vue@3.4.38(typescript@5.5.4))
@ -90,6 +99,12 @@ importers:
vue-tsc: vue-tsc:
specifier: ^2.1.4 specifier: ^2.1.4
version: 2.1.4(typescript@5.5.4) version: 2.1.4(typescript@5.5.4)
yaml:
specifier: ^2.5.1
version: 2.5.1
yup:
specifier: ^1.4.0
version: 1.4.0
packages: packages:
@ -1113,6 +1128,9 @@ packages:
'@iconify-json/bi@1.2.0': '@iconify-json/bi@1.2.0':
resolution: {integrity: sha512-kaBV87cQlyeMkBBiMqsf3b43Nsxdk/rYKvR29dnktht57WUyHCnBAuH+ca/bscX856CzRpVX+sYs7arjrJD0qA==} resolution: {integrity: sha512-kaBV87cQlyeMkBBiMqsf3b43Nsxdk/rYKvR29dnktht57WUyHCnBAuH+ca/bscX856CzRpVX+sYs7arjrJD0qA==}
'@iconify-json/eos-icons@1.2.0':
resolution: {integrity: sha512-grdfoS20Z4gWAzNPza7ytguNBWeTOkx4Y6aZHs149t2Z6AhW7zG3VWkkq6M+YuL2G8ugHnBw7ZxgazZ6oiMnIQ==}
'@iconify-json/heroicons-outline@1.2.0': '@iconify-json/heroicons-outline@1.2.0':
resolution: {integrity: sha512-Qy1sRmQYqih6xRxwCtnX0hXJ4252t83C0CnNWAP3gF0fH0Qmp9RY66LMB0moYGxQxUhsTFIl2nNceSVSBUo8Tg==} resolution: {integrity: sha512-Qy1sRmQYqih6xRxwCtnX0hXJ4252t83C0CnNWAP3gF0fH0Qmp9RY66LMB0moYGxQxUhsTFIl2nNceSVSBUo8Tg==}
@ -1122,8 +1140,8 @@ packages:
'@iconify-json/ic@1.2.0': '@iconify-json/ic@1.2.0':
resolution: {integrity: sha512-L4+m77xTQB08X3I+3xs1+IrvK+aNcuN/7ODu5aUPznHKLU+/8UYcsjUgNHze6vPOGPQ0AG+kCwvy91EYPXSRxw==} resolution: {integrity: sha512-L4+m77xTQB08X3I+3xs1+IrvK+aNcuN/7ODu5aUPznHKLU+/8UYcsjUgNHze6vPOGPQ0AG+kCwvy91EYPXSRxw==}
'@iconify-json/iconoir@1.2.0': '@iconify-json/iconoir@1.2.1':
resolution: {integrity: sha512-GbYAERFy9c1laIo1QZon9sBjRkX9rksirehmLIvvohn+W++fpPlvQnzVl0VpY/v2GCvs1pZ42spTKDj0AcofvA==} resolution: {integrity: sha512-x55gpORwMGkmmT9UO11rzfMOp40k0ggQnPiOoh9axbyuHrkFMN7pdoCbaXkzqAdShcoI1dLzARbdqXi2sAPJXQ==}
'@iconify-json/material-symbols@1.2.1': '@iconify-json/material-symbols@1.2.1':
resolution: {integrity: sha512-r9yaBzlUmN87aCTSoCNtDCd7R9F0iVDjNPL9QHHhm1WglFJvTUKx9iBC5xcZpP0qN0bg9R5FkM90CndWxEBAnw==} resolution: {integrity: sha512-r9yaBzlUmN87aCTSoCNtDCd7R9F0iVDjNPL9QHHhm1WglFJvTUKx9iBC5xcZpP0qN0bg9R5FkM90CndWxEBAnw==}
@ -1131,8 +1149,8 @@ packages:
'@iconify-json/mdi@1.2.0': '@iconify-json/mdi@1.2.0':
resolution: {integrity: sha512-E9/3l5Syg3wfuarorFodhn4s8YorxhH3U3U20LaNBNiqw1kFNIDWhF6HymuzAD35k7RH0OBasJ+ZUyFtVVV6eg==} resolution: {integrity: sha512-E9/3l5Syg3wfuarorFodhn4s8YorxhH3U3U20LaNBNiqw1kFNIDWhF6HymuzAD35k7RH0OBasJ+ZUyFtVVV6eg==}
'@iconify-json/simple-icons@1.2.2': '@iconify-json/simple-icons@1.2.3':
resolution: {integrity: sha512-VMgCoMnpvcCJ5b3rTOGPzW5j6959nIdRCk+8FGzK/vAaDd6f9sx65OcKOqP3C75llpybH/iQhk5yrJ/TOdQKeg==} resolution: {integrity: sha512-KZpqyPkDdyAvrpMz9/FYovwFsr3xrHf9esJeBng3iqNInhuZhF33XO1k6Yf8g2p10ynUW7AYEiGJkYzDi4DENQ==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@ -1360,6 +1378,113 @@ packages:
'@nuxtjs/color-mode@3.4.4': '@nuxtjs/color-mode@3.4.4':
resolution: {integrity: sha512-VSNJVGnRIjiGmfbMa0cN+rwNRowDRTL/wku/z5MpKSanVo3khIRitBNqNviso1l3T+LW0pLHeXBNp6L8g/l1EA==} resolution: {integrity: sha512-VSNJVGnRIjiGmfbMa0cN+rwNRowDRTL/wku/z5MpKSanVo3khIRitBNqNviso1l3T+LW0pLHeXBNp6L8g/l1EA==}
'@octokit/app@15.1.0':
resolution: {integrity: sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==}
engines: {node: '>= 18'}
'@octokit/auth-app@7.1.1':
resolution: {integrity: sha512-kRAd6yelV9OgvlEJE88H0VLlQdZcag9UlLr7dV0YYP37X8PPDvhgiTy66QVhDXdyoT0AleFN2w/qXkPdrSzINg==}
engines: {node: '>= 18'}
'@octokit/auth-oauth-app@8.1.1':
resolution: {integrity: sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==}
engines: {node: '>= 18'}
'@octokit/auth-oauth-device@7.1.1':
resolution: {integrity: sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==}
engines: {node: '>= 18'}
'@octokit/auth-oauth-user@5.1.1':
resolution: {integrity: sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==}
engines: {node: '>= 18'}
'@octokit/auth-token@5.1.1':
resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==}
engines: {node: '>= 18'}
'@octokit/auth-unauthenticated@6.1.0':
resolution: {integrity: sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==}
engines: {node: '>= 18'}
'@octokit/core@6.1.2':
resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==}
engines: {node: '>= 18'}
'@octokit/endpoint@10.1.1':
resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==}
engines: {node: '>= 18'}
'@octokit/graphql@8.1.1':
resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==}
engines: {node: '>= 18'}
'@octokit/oauth-app@7.1.3':
resolution: {integrity: sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==}
engines: {node: '>= 18'}
'@octokit/oauth-authorization-url@7.1.1':
resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==}
engines: {node: '>= 18'}
'@octokit/oauth-methods@5.1.2':
resolution: {integrity: sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==}
engines: {node: '>= 18'}
'@octokit/openapi-types@22.2.0':
resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
'@octokit/openapi-webhooks-types@8.3.0':
resolution: {integrity: sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==}
'@octokit/plugin-paginate-graphql@5.2.2':
resolution: {integrity: sha512-7znSVvlNAOJisCqAnjN1FtEziweOHSjPGAuc5W58NeGNAr/ZB57yCsjQbXDlWsVryA7hHQaEQPcBbJYFawlkyg==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-paginate-rest@11.3.3':
resolution: {integrity: sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-rest-endpoint-methods@13.2.4':
resolution: {integrity: sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-retry@7.1.1':
resolution: {integrity: sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': '>=6'
'@octokit/plugin-throttling@9.3.1':
resolution: {integrity: sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==}
engines: {node: '>= 18'}
peerDependencies:
'@octokit/core': ^6.0.0
'@octokit/request-error@6.1.4':
resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==}
engines: {node: '>= 18'}
'@octokit/request@9.1.3':
resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==}
engines: {node: '>= 18'}
'@octokit/types@13.5.0':
resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==}
'@octokit/webhooks-methods@5.1.0':
resolution: {integrity: sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==}
engines: {node: '>= 18'}
'@octokit/webhooks@13.3.0':
resolution: {integrity: sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==}
engines: {node: '>= 18'}
'@parcel/watcher-android-arm64@2.4.1': '@parcel/watcher-android-arm64@2.4.1':
resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -1708,6 +1833,9 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
'@types/aws-lambda@8.10.145':
resolution: {integrity: sha512-dtByW6WiFk5W5Jfgz1VM+YPA21xMXTuSFoLYIDY0L44jDLLflVPtZkYuu3/YxpGcvjzKFBZLU+GyKjR0HOYtyw==}
'@types/eslint-scope@3.7.6': '@types/eslint-scope@3.7.6':
resolution: {integrity: sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==} resolution: {integrity: sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==}
@ -1915,6 +2043,9 @@ packages:
peerDependencies: peerDependencies:
webpack: ^4 || ^5 webpack: ^4 || ^5
'@vee-validate/nuxt@4.13.2':
resolution: {integrity: sha512-rpYYO2isCrgHRdJnKRXGPn8aqhyZvCPUJgNubftPum1txbngkO/LNiFsZdFDO7auGprDAQf7axpszRfeKoCGVg==}
'@vercel/nft@0.26.5': '@vercel/nft@0.26.5':
resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==} resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -2272,6 +2403,9 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
before-after-hook@3.0.2:
resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
binary-extensions@2.2.0: binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2288,6 +2422,9 @@ packages:
boolbase@1.0.0: boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
brace-expansion@1.1.11: brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@ -3594,9 +3731,6 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
jsonfile@6.1.0: jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
@ -3855,9 +3989,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
mlly@1.4.2:
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
mlly@1.7.1: mlly@1.7.1:
resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==}
@ -4031,6 +4162,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
octokit@4.0.2:
resolution: {integrity: sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==}
engines: {node: '>= 18'}
ofetch@1.3.3: ofetch@1.3.3:
resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==} resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==}
@ -4205,9 +4340,6 @@ packages:
typescript: typescript:
optional: true optional: true
pkg-types@1.0.3:
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
pkg-types@1.2.0: pkg-types@1.2.0:
resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==}
@ -4431,6 +4563,9 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
property-expr@2.0.6:
resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
protocols@2.0.1: protocols@2.0.1:
resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==}
@ -4920,6 +5055,9 @@ packages:
text-table@0.2.0: text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
tiny-invariant@1.3.1: tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
@ -4957,6 +5095,9 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
toposort@2.0.2:
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
totalist@3.0.1: totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5001,10 +5142,18 @@ packages:
resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==}
engines: {node: '>=8'} engines: {node: '>=8'}
type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
type-fest@3.13.1: type-fest@3.13.1:
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
type-fest@4.26.1:
resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==}
engines: {node: '>=16'}
typescript@5.5.4: typescript@5.5.4:
resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
@ -5059,6 +5208,12 @@ packages:
resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
universal-github-app-jwt@2.2.0:
resolution: {integrity: sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==}
universal-user-agent@7.0.2:
resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==}
universalify@2.0.0: universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -5221,6 +5376,11 @@ packages:
resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
vee-validate@4.13.2:
resolution: {integrity: sha512-HlpR/6MJ92TW9f135umMZKUqdd/tFQTxLNSf2ImbU4Y/MlLVAUpF1l64VdjTOhbClAqPjCb5p/SqHDxLpUHXrw==}
peerDependencies:
vue: ^3.4.26
vite-hot-client@0.2.3: vite-hot-client@0.2.3:
resolution: {integrity: sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==} resolution: {integrity: sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==}
peerDependencies: peerDependencies:
@ -5523,8 +5683,8 @@ packages:
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@2.5.0: yaml@2.5.1:
resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
hasBin: true hasBin: true
@ -5544,6 +5704,9 @@ packages:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
yup@1.4.0:
resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==}
zhead@2.2.4: zhead@2.2.4:
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
@ -6466,6 +6629,10 @@ snapshots:
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify-json/eos-icons@1.2.0':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/heroicons-outline@1.2.0': '@iconify-json/heroicons-outline@1.2.0':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
@ -6478,7 +6645,7 @@ snapshots:
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify-json/iconoir@1.2.0': '@iconify-json/iconoir@1.2.1':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
@ -6490,7 +6657,7 @@ snapshots:
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify-json/simple-icons@1.2.2': '@iconify-json/simple-icons@1.2.3':
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
@ -6832,9 +6999,9 @@ snapshots:
ignore: 5.2.4 ignore: 5.2.4
jiti: 1.20.0 jiti: 1.20.0
knitwork: 1.0.0 knitwork: 1.0.0
mlly: 1.4.2 mlly: 1.7.1
pathe: 1.1.1 pathe: 1.1.1
pkg-types: 1.0.3 pkg-types: 1.2.0
scule: 1.0.0 scule: 1.0.0
semver: 7.5.4 semver: 7.5.4
ufo: 1.3.1 ufo: 1.3.1
@ -6870,7 +7037,7 @@ snapshots:
defu: 6.1.2 defu: 6.1.2
hookable: 5.5.3 hookable: 5.5.3
pathe: 1.1.1 pathe: 1.1.1
pkg-types: 1.0.3 pkg-types: 1.2.0
postcss-import-resolver: 2.0.0 postcss-import-resolver: 2.0.0
std-env: 3.4.3 std-env: 3.4.3
ufo: 1.3.1 ufo: 1.3.1
@ -7013,6 +7180,152 @@ snapshots:
- rollup - rollup
- supports-color - supports-color
'@octokit/app@15.1.0':
dependencies:
'@octokit/auth-app': 7.1.1
'@octokit/auth-unauthenticated': 6.1.0
'@octokit/core': 6.1.2
'@octokit/oauth-app': 7.1.3
'@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2)
'@octokit/types': 13.5.0
'@octokit/webhooks': 13.3.0
'@octokit/auth-app@7.1.1':
dependencies:
'@octokit/auth-oauth-app': 8.1.1
'@octokit/auth-oauth-user': 5.1.1
'@octokit/request': 9.1.3
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
lru-cache: 10.4.3
universal-github-app-jwt: 2.2.0
universal-user-agent: 7.0.2
'@octokit/auth-oauth-app@8.1.1':
dependencies:
'@octokit/auth-oauth-device': 7.1.1
'@octokit/auth-oauth-user': 5.1.1
'@octokit/request': 9.1.3
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/auth-oauth-device@7.1.1':
dependencies:
'@octokit/oauth-methods': 5.1.2
'@octokit/request': 9.1.3
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/auth-oauth-user@5.1.1':
dependencies:
'@octokit/auth-oauth-device': 7.1.1
'@octokit/oauth-methods': 5.1.2
'@octokit/request': 9.1.3
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/auth-token@5.1.1': {}
'@octokit/auth-unauthenticated@6.1.0':
dependencies:
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
'@octokit/core@6.1.2':
dependencies:
'@octokit/auth-token': 5.1.1
'@octokit/graphql': 8.1.1
'@octokit/request': 9.1.3
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
before-after-hook: 3.0.2
universal-user-agent: 7.0.2
'@octokit/endpoint@10.1.1':
dependencies:
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/graphql@8.1.1':
dependencies:
'@octokit/request': 9.1.3
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/oauth-app@7.1.3':
dependencies:
'@octokit/auth-oauth-app': 8.1.1
'@octokit/auth-oauth-user': 5.1.1
'@octokit/auth-unauthenticated': 6.1.0
'@octokit/core': 6.1.2
'@octokit/oauth-authorization-url': 7.1.1
'@octokit/oauth-methods': 5.1.2
'@types/aws-lambda': 8.10.145
universal-user-agent: 7.0.2
'@octokit/oauth-authorization-url@7.1.1': {}
'@octokit/oauth-methods@5.1.2':
dependencies:
'@octokit/oauth-authorization-url': 7.1.1
'@octokit/request': 9.1.3
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
'@octokit/openapi-types@22.2.0': {}
'@octokit/openapi-webhooks-types@8.3.0': {}
'@octokit/plugin-paginate-graphql@5.2.2(@octokit/core@6.1.2)':
dependencies:
'@octokit/core': 6.1.2
'@octokit/plugin-paginate-rest@11.3.3(@octokit/core@6.1.2)':
dependencies:
'@octokit/core': 6.1.2
'@octokit/types': 13.5.0
'@octokit/plugin-rest-endpoint-methods@13.2.4(@octokit/core@6.1.2)':
dependencies:
'@octokit/core': 6.1.2
'@octokit/types': 13.5.0
'@octokit/plugin-retry@7.1.1(@octokit/core@6.1.2)':
dependencies:
'@octokit/core': 6.1.2
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
bottleneck: 2.19.5
'@octokit/plugin-throttling@9.3.1(@octokit/core@6.1.2)':
dependencies:
'@octokit/core': 6.1.2
'@octokit/types': 13.5.0
bottleneck: 2.19.5
'@octokit/request-error@6.1.4':
dependencies:
'@octokit/types': 13.5.0
'@octokit/request@9.1.3':
dependencies:
'@octokit/endpoint': 10.1.1
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
universal-user-agent: 7.0.2
'@octokit/types@13.5.0':
dependencies:
'@octokit/openapi-types': 22.2.0
'@octokit/webhooks-methods@5.1.0': {}
'@octokit/webhooks@13.3.0':
dependencies:
'@octokit/openapi-webhooks-types': 8.3.0
'@octokit/request-error': 6.1.4
'@octokit/webhooks-methods': 5.1.0
'@parcel/watcher-android-arm64@2.4.1': '@parcel/watcher-android-arm64@2.4.1':
optional: true optional: true
@ -7282,6 +7595,8 @@ snapshots:
'@trysound/sax@0.2.0': {} '@trysound/sax@0.2.0': {}
'@types/aws-lambda@8.10.145': {}
'@types/eslint-scope@3.7.6': '@types/eslint-scope@3.7.6':
dependencies: dependencies:
'@types/eslint': 9.6.1 '@types/eslint': 9.6.1
@ -7628,6 +7943,17 @@ snapshots:
- rollup - rollup
- supports-color - supports-color
'@vee-validate/nuxt@4.13.2(magicast@0.3.5)(rollup@4.21.2)(vue@3.4.38(typescript@5.5.4))':
dependencies:
'@nuxt/kit': 3.13.0(magicast@0.3.5)(rollup@4.21.2)
local-pkg: 0.5.0
vee-validate: 4.13.2(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies:
- magicast
- rollup
- supports-color
- vue
'@vercel/nft@0.26.5(encoding@0.1.13)': '@vercel/nft@0.26.5(encoding@0.1.13)':
dependencies: dependencies:
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
@ -8141,6 +8467,8 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
before-after-hook@3.0.2: {}
binary-extensions@2.2.0: {} binary-extensions@2.2.0: {}
bindings@1.5.0: bindings@1.5.0:
@ -8158,6 +8486,8 @@ snapshots:
boolbase@1.0.0: {} boolbase@1.0.0: {}
bottleneck@2.19.5: {}
brace-expansion@1.1.11: brace-expansion@1.1.11:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@ -8251,11 +8581,11 @@ snapshots:
dotenv: 16.3.1 dotenv: 16.3.1
giget: 1.1.3 giget: 1.1.3
jiti: 1.20.0 jiti: 1.20.0
mlly: 1.4.2 mlly: 1.7.1
ohash: 1.1.3 ohash: 1.1.3
pathe: 1.1.1 pathe: 1.1.1
perfect-debounce: 1.0.0 perfect-debounce: 1.0.0
pkg-types: 1.0.3 pkg-types: 1.2.0
rc9: 2.1.1 rc9: 2.1.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -9649,8 +9979,6 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jsonc-parser@3.2.0: {}
jsonfile@6.1.0: jsonfile@6.1.0:
dependencies: dependencies:
universalify: 2.0.0 universalify: 2.0.0
@ -9722,7 +10050,7 @@ snapshots:
local-pkg@0.5.0: local-pkg@0.5.0:
dependencies: dependencies:
mlly: 1.4.2 mlly: 1.7.1
pkg-types: 1.2.0 pkg-types: 1.2.0
locate-path@5.0.0: locate-path@5.0.0:
@ -9905,13 +10233,6 @@ snapshots:
mkdirp@1.0.4: {} mkdirp@1.0.4: {}
mlly@1.4.2:
dependencies:
acorn: 8.10.0
pathe: 1.1.1
pkg-types: 1.0.3
ufo: 1.3.1
mlly@1.7.1: mlly@1.7.1:
dependencies: dependencies:
acorn: 8.12.1 acorn: 8.12.1
@ -10261,6 +10582,19 @@ snapshots:
object-assign@4.1.1: {} object-assign@4.1.1: {}
octokit@4.0.2:
dependencies:
'@octokit/app': 15.1.0
'@octokit/core': 6.1.2
'@octokit/oauth-app': 7.1.3
'@octokit/plugin-paginate-graphql': 5.2.2(@octokit/core@6.1.2)
'@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2)
'@octokit/plugin-rest-endpoint-methods': 13.2.4(@octokit/core@6.1.2)
'@octokit/plugin-retry': 7.1.1(@octokit/core@6.1.2)
'@octokit/plugin-throttling': 9.3.1(@octokit/core@6.1.2)
'@octokit/request-error': 6.1.4
'@octokit/types': 13.5.0
ofetch@1.3.3: ofetch@1.3.3:
dependencies: dependencies:
destr: 2.0.2 destr: 2.0.2
@ -10429,12 +10763,6 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.5.4 typescript: 5.5.4
pkg-types@1.0.3:
dependencies:
jsonc-parser: 3.2.0
mlly: 1.4.2
pathe: 1.1.1
pkg-types@1.2.0: pkg-types@1.2.0:
dependencies: dependencies:
confbox: 0.1.7 confbox: 0.1.7
@ -10653,6 +10981,8 @@ snapshots:
kleur: 3.0.3 kleur: 3.0.3
sisteransi: 1.0.5 sisteransi: 1.0.5
property-expr@2.0.6: {}
protocols@2.0.1: {} protocols@2.0.1: {}
prr@1.0.1: {} prr@1.0.1: {}
@ -11215,6 +11545,8 @@ snapshots:
text-table@0.2.0: {} text-table@0.2.0: {}
tiny-case@1.0.3: {}
tiny-invariant@1.3.1: {} tiny-invariant@1.3.1: {}
tinybench@2.9.0: {} tinybench@2.9.0: {}
@ -11240,6 +11572,8 @@ snapshots:
toidentifier@1.0.1: {} toidentifier@1.0.1: {}
toposort@2.0.2: {}
totalist@3.0.1: {} totalist@3.0.1: {}
tr46@0.0.3: {} tr46@0.0.3: {}
@ -11274,8 +11608,12 @@ snapshots:
type-fest@0.8.1: {} type-fest@0.8.1: {}
type-fest@2.19.0: {}
type-fest@3.13.1: {} type-fest@3.13.1: {}
type-fest@4.26.1: {}
typescript@5.5.4: {} typescript@5.5.4: {}
ufo@1.3.1: {} ufo@1.3.1: {}
@ -11349,9 +11687,9 @@ snapshots:
fast-glob: 3.3.1 fast-glob: 3.3.1
local-pkg: 0.4.3 local-pkg: 0.4.3
magic-string: 0.30.5 magic-string: 0.30.5
mlly: 1.4.2 mlly: 1.7.1
pathe: 1.1.1 pathe: 1.1.1
pkg-types: 1.0.3 pkg-types: 1.2.0
scule: 1.0.0 scule: 1.0.0
strip-literal: 1.3.0 strip-literal: 1.3.0
unplugin: 1.5.0 unplugin: 1.5.0
@ -11366,6 +11704,10 @@ snapshots:
dependencies: dependencies:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
universal-github-app-jwt@2.2.0: {}
universal-user-agent@7.0.2: {}
universalify@2.0.0: {} universalify@2.0.0: {}
unocss@0.62.3(@unocss/webpack@0.62.3(rollup@4.21.2)(webpack@5.89.0(esbuild@0.23.1)))(postcss@8.4.44)(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0)): unocss@0.62.3(@unocss/webpack@0.62.3(rollup@4.21.2)(webpack@5.89.0(esbuild@0.23.1)))(postcss@8.4.44)(rollup@4.21.2)(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0)):
@ -11413,7 +11755,7 @@ snapshots:
pathe: 1.1.2 pathe: 1.1.2
scule: 1.3.0 scule: 1.3.0
unplugin: 1.12.3 unplugin: 1.12.3
yaml: 2.5.0 yaml: 2.5.1
optionalDependencies: optionalDependencies:
vue-router: 4.4.3(vue@3.4.38(typescript@5.5.4)) vue-router: 4.4.3(vue@3.4.38(typescript@5.5.4))
transitivePeerDependencies: transitivePeerDependencies:
@ -11544,6 +11886,12 @@ snapshots:
dependencies: dependencies:
builtins: 5.0.1 builtins: 5.0.1
vee-validate@4.13.2(vue@3.4.38(typescript@5.5.4)):
dependencies:
'@vue/devtools-api': 6.6.3
type-fest: 4.26.1
vue: 3.4.38(typescript@5.5.4)
vite-hot-client@0.2.3(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0)): vite-hot-client@0.2.3(vite@5.4.2(@types/node@20.8.7)(terser@5.22.0)):
dependencies: dependencies:
vite: 5.4.2(@types/node@20.8.7)(terser@5.22.0) vite: 5.4.2(@types/node@20.8.7)(terser@5.22.0)
@ -11860,7 +12208,7 @@ snapshots:
yallist@4.0.0: {} yallist@4.0.0: {}
yaml@2.5.0: {} yaml@2.5.1: {}
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
@ -11878,6 +12226,13 @@ snapshots:
yocto-queue@1.0.0: {} yocto-queue@1.0.0: {}
yup@1.4.0:
dependencies:
property-expr: 2.0.6
tiny-case: 1.0.3
toposort: 2.0.2
type-fest: 2.19.0
zhead@2.2.4: {} zhead@2.2.4: {}
zip-stream@6.0.1: zip-stream@6.0.1:

135
server/api/data.post.ts Normal file
View File

@ -0,0 +1,135 @@
import { App } from 'octokit'
import yaml from 'yaml'
import type { Project } from '~/types'
export default defineEventHandler(async (event) => {
const body = await readBody<{ project: Project, image?: { type: string, data: string } }>(event)
const yamlProject = yaml.stringify(body.project)
const { appId, privateKey, installationId } = useAppConfig().github
const id = body.project.id || body.project.name.toLowerCase().replace(/\s+/g, '-')
const app = new App({
appId,
privateKey,
})
await app.octokit.rest.apps.getAuthenticated()
const octokit = await app.getInstallationOctokit(installationId)
const owner = 'develit-io'
const repo = 'test-repo'
const baseBranch = 'main'
const newBranchName = `${id}-project-update-${Date.now()}`
const commitMessage = `${body.project.id ? `Updating the project: ${body.project.name}` : `Initiating the creation of project: ${body.project.name}`}`
const files = [
{
path: `src/projects/${id}/index.yaml`,
content: yamlProject,
encoding: 'utf-8',
},
]
if (body.image?.data && body.image?.type) {
files.push(
{
path: `src/projects/${id}/logo.${body.image.type.split('/')[1]}`,
content: body.image.data,
encoding: 'base64',
},
)
}
async function createBranch(owner: string, repo: string, newBranchName: string, baseBranch: string) {
const { data: baseBranchData } = await octokit.rest.git.getRef({
owner,
repo,
ref: `heads/${baseBranch}`,
})
await octokit.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${newBranchName}`,
sha: baseBranchData.object.sha,
})
}
async function commitChangesToNewBranch(owner: string, repo: string, newBranch: string, message: string, files: { path: string, content: string, encoding: string }[]) {
const { data: latestCommit } = await octokit.rest.repos.getCommit({
owner,
repo,
ref: newBranch,
})
const { data: baseTree } = await octokit.rest.git.getTree({
owner,
repo,
tree_sha: latestCommit.commit.tree.sha,
})
const blobs = await Promise.all(files.map(async (file) => {
const { data: blob } = await octokit.rest.git.createBlob({
owner,
repo,
content: file.content,
encoding: file.encoding,
})
return {
path: file.path,
mode: '100644' as const,
type: 'blob' as const,
sha: blob.sha,
}
}))
const { data: newTree } = await octokit.rest.git.createTree({
owner,
repo,
base_tree: baseTree.sha,
tree: blobs,
})
const { data: newCommit } = await octokit.rest.git.createCommit({
owner,
repo,
message,
tree: newTree.sha,
parents: [latestCommit.sha],
})
await octokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${newBranch}`,
sha: newCommit.sha,
})
}
async function createPullRequest(owner: string, repo: string, head: string, base: string, title: string, body: string) {
const { data: pullRequest } = await octokit.rest.pulls.create({
owner,
repo,
title,
head,
base,
body,
})
return pullRequest
}
try {
await createBranch(owner, repo, newBranchName, baseBranch)
console.log(`Branch ${newBranchName} created successfully!`)
await commitChangesToNewBranch(owner, repo, newBranchName, commitMessage, files)
console.log(`Changes committed to branch ${newBranchName} successfully!`)
const pullRequestData = await createPullRequest(owner, repo, newBranchName, baseBranch, `${body.project.id ? `Update project: ${body.project.name}` : `Create project: ${body.project.name}`}`, `${body.project.id ? `Updating the project: ${body.project.name}` : `Initiating the creation of project: ${body.project.name}`}`)
console.log('Pull request created:', pullRequestData)
}
catch (error) {
console.error('Error during GitHub operations:', error)
}
})

View File

@ -16084,6 +16084,48 @@
"name": "Other" "name": "Other"
} }
], ],
"project_phase": [
{
"id": "mainnet",
"name": "Mainnet"
},
{
"id": "testnet",
"name": "Testnet"
},
{
"id": "alpha",
"name": "Alpha"
},
{
"id": "beta",
"name": "Beta"
},
{
"id": "mvp",
"name": "MVP"
}
],
"asset_custody_type": [
{
"id": "non-custody",
"name": "Non-custody"
}
],
"sign_in_type_requirments": [
{
"id": "wallet",
"name": "Wallet"
},
{
"id": "email",
"name": "Email"
},
{
"id": "seed",
"name": "Seed"
}
],
"assets": [ "assets": [
{ {
"id": "eth", "id": "eth",

View File

@ -1,6 +1,6 @@
export interface Category { export interface Category {
id: string id: string
name: string name: string
usecases: string[]
projectsCount: number projectsCount: number
usecases?: string[]
} }

View File

@ -1,5 +1,5 @@
export interface InputOption { export interface InputOption {
label: string label: string
value: string value: string | number
count?: number count?: number
} }

View File

@ -9,7 +9,7 @@ export interface Member {
role?: string role?: string
link?: string link?: string
[k: string]: unknown [k: string]: unknown
} }[]
company?: { company?: {
name?: string name?: string
link?: string link?: string

View File

@ -1,4 +1,4 @@
import type { Member } from './member' import type { Team } from './team'
import type { Fund } from './fund' import type { Fund } from './fund'
import type { ClientDiversability } from './clientDiversability' import type { ClientDiversability } from './clientDiversability'
import type { Audit } from './audit' import type { Audit } from './audit'
@ -16,15 +16,17 @@ export interface Project {
assets_used?: string[] assets_used?: string[]
tokens?: { tokens?: {
name?: string name?: string
symbol: string symbol?: string
network?: string network?: string
contract_address?: string contract_address?: string
link?: string token_link?: string
[k: string]: unknown [k: string]: unknown
}[] }[]
description?: string description?: string
project_type?: string project_type?: string
product_launch_day?: string product_launch_day?: string
project_phase?: string
sunset: boolean
technology?: { technology?: {
type: string type: string
name?: string name?: string
@ -65,17 +67,17 @@ export interface Project {
} }
[k: string]: unknown [k: string]: unknown
opensource: boolean opensource: boolean
viewing_key: boolean viewing_key?: boolean
dissapearing_tx: boolean dissapearing_tx?: boolean
frontend_anonymity: string frontend_anonymity?: string
identity_integration: null identity_integration?: null
connected_tx: boolean connected_tx?: boolean
revealed_recipient: boolean revealed_recipient?: boolean
revealed_sender: boolean revealed_sender?: boolean
revealed_ammount: boolean revealed_ammount?: boolean
reversability_condition: string reversability_condition?: string
data_masking: string data_masking?: string
asset_custody_type: string asset_custody_type?: string
} }
licences?: string licences?: string
privacy_policy?: { privacy_policy?: {
@ -84,7 +86,7 @@ export interface Project {
data_usage?: string data_usage?: string
[k: string]: unknown [k: string]: unknown
} }
team?: Member[] team?: Team
storage?: { storage?: {
decentralized?: boolean decentralized?: boolean
[k: string]: unknown [k: string]: unknown
@ -92,7 +94,7 @@ export interface Project {
tracebility?: { tracebility?: {
tracked_data?: string tracked_data?: string
kyc?: boolean kyc?: boolean
sign_in_type_requirments?: string sign_in_type_requirments?: string[]
[k: string]: unknown [k: string]: unknown
} }
third_party_dependency?: string third_party_dependency?: string
@ -107,7 +109,7 @@ export interface Project {
time?: string time?: string
link?: string link?: string
[k: string]: unknown [k: string]: unknown
} }[]
client_diversability?: ClientDiversability[] client_diversability?: ClientDiversability[]
default_privacy?: boolean default_privacy?: boolean
funding?: Fund[] funding?: Fund[]
@ -133,23 +135,23 @@ export interface ProjectShallow {
title1: string title1: string
description: string description: string
percentage: number percentage: number
forum?: string
github?: string
website?: string
twitter?: string
coingecko?: string
explorer?: string
newsletter?: string
readyness?: string
team?: Team
docs?: string
audits?: Audit[]
support?: number
anonymity?: boolean
categories: string[] categories: string[]
usecases?: string[] usecases?: string[]
ecosystem?: string[] ecosystem?: string[]
assets_used?: string [] assets_used?: string []
forum?: string | undefined
github?: string | undefined
website?: string | undefined
twitter?: string | undefined
coingecko?: string | undefined
explorer?: string | undefined
newsletter?: string | undefined
readyness?: string | undefined
team: Member[] | undefined
docs: string | undefined
audits?: Audit[] | undefined
support?: number | undefined
anonymity?: boolean | undefined
ratings?: ProjectRating[] ratings?: ProjectRating[]
} }

19
types/team.ts Normal file
View File

@ -0,0 +1,19 @@
export interface Team {
name?: string
role?: string
link?: string
avatar?: string
anonymous?: boolean
teammembers?: {
name?: string
role?: string
link?: string
[k: string]: unknown
}[]
company?: {
name?: string
link?: string
contacts?: string
[k: string]: unknown
}
}

View File

@ -47,6 +47,7 @@ export default defineConfig({
black: '#000', black: '#000',
green: '#B5E26B', green: '#B5E26B',
red: '#FF5252', red: '#FF5252',
danger: '#FF0000',
bg: { bg: {
grey: '#ffffff33', grey: '#ffffff33',
dark_grey: '#161616', dark_grey: '#161616',