| @@ -0,0 +1,21 @@ | |||||
| #!/usr/bin/env bash | |||||
| set -euo pipefail | |||||
| OPENAPI_URL="${OPENAPI_URL:-http://127.0.0.1:7300/openapi.json}" | |||||
| SPEC_FILE="openapi.fetched.json" | |||||
| OUTPUT_DIR="src/generated/typescript" | |||||
| cleanup() { | |||||
| rm -f "$SPEC_FILE" | |||||
| } | |||||
| trap cleanup EXIT | |||||
| curl --fail --silent --show-error \ | |||||
| --header "Accept: application/json" \ | |||||
| "$OPENAPI_URL" \ | |||||
| --output "$SPEC_FILE" | |||||
| npx openapi-typescript-codegen \ | |||||
| --input "$SPEC_FILE" \ | |||||
| --output "$OUTPUT_DIR" | |||||
| @@ -6,8 +6,7 @@ | |||||
| "scripts": { | "scripts": { | ||||
| "dev": "vite", | "dev": "vite", | ||||
| "build": "vue-tsc -b && vite build", | "build": "vue-tsc -b && vite build", | ||||
| "preview": "vite preview", | |||||
| "genapi": "npx openapi-typescript-codegen --input http://127.0.0.1:7300/openapi.json --output ./src/generated/typescript" | |||||
| "preview": "vite preview" | |||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "@stellar/freighter-api": "^4.1.0", | "@stellar/freighter-api": "^4.1.0", | ||||
| @@ -51,8 +51,8 @@ import AmendmentModal from "./modals/AmendmentModal.vue"; | |||||
| import {getChipClass} from "../utils/amendment.ts"; | import {getChipClass} from "../utils/amendment.ts"; | ||||
| import {AmendmentService} from "../generated/typescript"; | import {AmendmentService} from "../generated/typescript"; | ||||
| const props = defineProps<{issueId?: number}>(); | |||||
| const {issueId} = toRefs(props); | |||||
| const props = defineProps<{proposalId?: number}>(); | |||||
| const {proposalId} = toRefs(props); | |||||
| const offset = ref(0); | const offset = ref(0); | ||||
| const amendments = ref<Amendment[]>(); | const amendments = ref<Amendment[]>(); | ||||
| @@ -65,8 +65,8 @@ const showAmendment = async (amendmentId: number, status: string) => { | |||||
| } | } | ||||
| const getAmendments = async () => { | const getAmendments = async () => { | ||||
| if (!props.issueId) return; | |||||
| const resp = await AmendmentService.listAmendments(props.issueId) | |||||
| if (!props.proposalId) return; | |||||
| const resp = await AmendmentService.listAmendments(props.proposalId) | |||||
| amendments.value = resp.amendments; | amendments.value = resp.amendments; | ||||
| } | } | ||||
| @@ -111,7 +111,7 @@ const scrolledMax = () => { | |||||
| const numberOfAmendments = computed(() => amendments.value?.length || 0); | const numberOfAmendments = computed(() => amendments.value?.length || 0); | ||||
| watch(issueId, async (newVal) => { | |||||
| watch(proposalId, async (newVal) => { | |||||
| if (!newVal) return; | if (!newVal) return; | ||||
| await getAmendments(); | await getAmendments(); | ||||
| offset.value = 0; | offset.value = 0; | ||||
| @@ -1,7 +1,8 @@ | |||||
| <template> | <template> | ||||
| <div class="flex flex-row justify-between bg-gray-500 drop-shadow-md px-3 py-2"> | <div class="flex flex-row justify-between bg-gray-500 drop-shadow-md px-3 py-2"> | ||||
| <span class="text-4xl font-header font-bold"> | <span class="text-4xl font-header font-bold"> | ||||
| <ColoredHeader text="Ikibani" from="#F2F3E2" to="#B2E5F8" /> | |||||
| <!-- https://senara.network --> | |||||
| <ColoredHeader text="Senara" from="#F2F3E2" to="#B2E5F8" /> | |||||
| </span> | </span> | ||||
| <div class="flex flex-col justify-center items-center"> | <div class="flex flex-col justify-center items-center"> | ||||
| <button | <button | ||||
| @@ -1,47 +0,0 @@ | |||||
| <template> | |||||
| <div class="p-8 my-px hover:bg-amber-100 cursor-pointer" :class="{'bg-amber-300': selected, 'bg-amber-50': !selected}" @click="handleSelection"> | |||||
| <div class="mb-2"> | |||||
| <span class=" block font-bold text-xl font-header whitespace-nowrap overflow-hidden overflow-ellipsis">{{ issue.name }}</span> | |||||
| <span class="text-blue-600 text-xs py-1 px-1.5 bg-amber-50 drop-shadow rounded-sm italic">{{ truncateWallet(issue.creator) }}</span> | |||||
| </div> | |||||
| <div class="max-h-12 overflow-hidden overflow-ellipsis"> | |||||
| <span | |||||
| class="text-sm text-gray-600 leading-tighter text-justify" | |||||
| v-html="renderMarkdown(issue.summary)" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </template> | |||||
| <script setup lang="ts"> | |||||
| import {IssueSelection} from "../types/issue.ts"; | |||||
| import {truncateWallet} from "../utils/wallet.ts"; | |||||
| import DOMPurify from "dompurify"; | |||||
| import {parse} from "marked"; | |||||
| import {useLoading} from "../composables/useLoading.ts"; | |||||
| import {ProposalService, ProposalItem} from "../generated/typescript"; | |||||
| const {show: showLoading, hide: hideLoading} = useLoading(); | |||||
| const props = defineProps<{ issue: ProposalItem, selected: boolean }>(); | |||||
| const emits = defineEmits<{ | |||||
| (e: 'selected', selection: IssueSelection): void | |||||
| }>(); | |||||
| const handleSelection = async () => { | |||||
| showLoading('content'); | |||||
| const proposalContent = await ProposalService.getProposal(props.issue.id); | |||||
| proposalContent.proposal.id = props.issue.id; | |||||
| emits('selected', {proposal: proposalContent.proposal, cid: props.issue.cid}); | |||||
| hideLoading('content'); | |||||
| } | |||||
| const renderMarkdown = (markdown?: string | null) => { | |||||
| if (!markdown) return; | |||||
| const parsed = parse(markdown) as string; | |||||
| return DOMPurify.sanitize(parsed); | |||||
| } | |||||
| </script> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -22,19 +22,18 @@ | |||||
| import {Editor, EditorContent} from '@tiptap/vue-3' | import {Editor, EditorContent} from '@tiptap/vue-3' | ||||
| import StarterKit from '@tiptap/starter-kit' | import StarterKit from '@tiptap/starter-kit' | ||||
| import {onBeforeUnmount} from 'vue' | import {onBeforeUnmount} from 'vue' | ||||
| import Underline from '@tiptap/extension-underline' | |||||
| import TurndownService from 'turndown'; | import TurndownService from 'turndown'; | ||||
| import {ProposalParagraph} from "../types/proposal.ts"; | |||||
| const turndownService = new TurndownService(); | const turndownService = new TurndownService(); | ||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||
| 'update:content': [content: string] | |||||
| 'update:content': [content: ProposalParagraph[]], | |||||
| }>() | }>() | ||||
| const editor = new Editor({ | const editor = new Editor({ | ||||
| extensions: [ | extensions: [ | ||||
| StarterKit, | StarterKit, | ||||
| Underline, | |||||
| ], | ], | ||||
| content: '', | content: '', | ||||
| editorProps: { | editorProps: { | ||||
| @@ -45,7 +44,8 @@ const editor = new Editor({ | |||||
| onUpdate: ({ editor }) => { | onUpdate: ({ editor }) => { | ||||
| const html = editor.getHTML(); | const html = editor.getHTML(); | ||||
| const content = convertEditorContentToMarkdown(html); | const content = convertEditorContentToMarkdown(html); | ||||
| emit('update:content', content); | |||||
| const paragraphs = content.split('\n').map(p => p.trim()).filter(p => p.length > 0).map((p, i) => ({paragraphText: p, index: i})); | |||||
| emit('update:content', paragraphs); | |||||
| } | } | ||||
| }) | }) | ||||
| @@ -0,0 +1,74 @@ | |||||
| <template> | |||||
| <article | |||||
| class="p-8 my-px hover:bg-amber-100 cursor-pointer" | |||||
| :class="{'bg-amber-300': selected, 'bg-amber-50': !selected}" | |||||
| @click="handleSelection" | |||||
| > | |||||
| <header class="min-w-0"> | |||||
| <h3 | |||||
| class="truncate font-header text-xl font-bold text-slate-950" | |||||
| :title="proposal.name" | |||||
| > | |||||
| {{ proposal.name }} | |||||
| </h3> | |||||
| </header> | |||||
| <div class="mt-2 text-sm leading-5 text-slate-800/80 max-h-16 overflow-hidden" v-html="renderMarkdown(proposal.summary)" /> | |||||
| <footer | |||||
| class="mt-auto flex min-w-0 items-center gap-1.5 pt-4 text-slate-900/70" | |||||
| :title="proposal.creator" | |||||
| > | |||||
| <Identification | |||||
| class="size-3.5 shrink-0" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| <span class="pb-0.5"> | |||||
| <!-- Attribution --> | |||||
| <span class="text-[0.6rem] font-bold uppercase tracking-[0.12em]"> | |||||
| Created by | |||||
| </span> | |||||
| <span class="text-[0.6rem] truncate text-xs font-semibold tracking-wide"> | |||||
| {{ truncateWallet(proposal.creator) }} | |||||
| </span> | |||||
| </span> | |||||
| </footer> | |||||
| </article> | |||||
| </template> | |||||
| <script setup lang="ts"> | |||||
| import {ProposalSelection} from "../types/proposal.ts"; | |||||
| import {truncateWallet} from "../utils/wallet.ts"; | |||||
| import DOMPurify from "dompurify"; | |||||
| import {parse} from "marked"; | |||||
| import {useLoading} from "../composables/useLoading.ts"; | |||||
| import {ProposalService, ProposalItem} from "../generated/typescript"; | |||||
| import Identification from "./icons/Identification.vue"; | |||||
| const {show: showLoading, hide: hideLoading} = useLoading(); | |||||
| const props = defineProps<{ proposal: ProposalItem, selected: boolean }>(); | |||||
| const emits = defineEmits<{ | |||||
| (e: 'selected', selection: ProposalSelection): void | |||||
| }>(); | |||||
| const handleSelection = async () => { | |||||
| showLoading('content'); | |||||
| const proposalContent = await ProposalService.getProposal(props.proposal.id); | |||||
| proposalContent.proposal.id = props.proposal.id; | |||||
| emits('selected', {proposal: proposalContent.proposal}); | |||||
| hideLoading('content'); | |||||
| } | |||||
| const renderMarkdown = (markdown?: string | null) => { | |||||
| if (!markdown) return; | |||||
| const parsed = parse(markdown) as string; | |||||
| return DOMPurify.sanitize(parsed); | |||||
| } | |||||
| </script> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -1,11 +1,11 @@ | |||||
| <template> | <template> | ||||
| <aside class="sm:flex sm:flex-col sm:w-1/4 bg-gray-600 h-screen"> | <aside class="sm:flex sm:flex-col sm:w-1/4 bg-gray-600 h-screen"> | ||||
| <nav class="flex flex-row w-full h-1/16 justify-between"> | |||||
| <nav class="sticky top-0 left-0 flex flex-row w-full h-1/16 justify-between"> | |||||
| <button | <button | ||||
| class="w-10/12 h-full bg-gray-800 px-5 py-3 text-sm font-bold text-white border-r border-white border-solid uppercase text-center cursor-pointer" | class="w-10/12 h-full bg-gray-800 px-5 py-3 text-sm font-bold text-white border-r border-white border-solid uppercase text-center cursor-pointer" | ||||
| @click="showNewIssueModal" | |||||
| @click="showNewProposalModal" | |||||
| > | > | ||||
| Add Issue | |||||
| Add Proposal | |||||
| </button> | </button> | ||||
| <button | <button | ||||
| class="flex flex-row w-2/12 h-full bg-gray-800 justify-center items-center cursor-pointer" | class="flex flex-row w-2/12 h-full bg-gray-800 justify-center items-center cursor-pointer" | ||||
| @@ -15,28 +15,28 @@ | |||||
| </button> | </button> | ||||
| </nav> | </nav> | ||||
| <div class="h-full overflow-y-scroll"> | |||||
| <IssueContainer | |||||
| v-for="issue in issues" | |||||
| :key="issue.id" | |||||
| :issue="issue" | |||||
| :selected="selectedId === issue.id" | |||||
| <div class="h-11/12"> | |||||
| <ProposalContainer | |||||
| v-for="proposal in proposals" | |||||
| :key="proposal.id" | |||||
| :proposal="proposal" | |||||
| :selected="selectedId === proposal.id" | |||||
| @selected="handleSelection" | @selected="handleSelection" | ||||
| /> | /> | ||||
| <LoadingSpinner | <LoadingSpinner | ||||
| v-if="isLoading('issues')" | |||||
| identifier="issues" | |||||
| v-if="isLoading('proposals')" | |||||
| identifier="proposals" | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| </aside> | </aside> | ||||
| </template> | </template> | ||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||
| import IssueContainer from "./IssueContainer.vue"; | |||||
| import ProposalContainer from "./ProposalContainer.vue"; | |||||
| import LoadingSpinner from "./LoadingSpinner.vue"; | import LoadingSpinner from "./LoadingSpinner.vue"; | ||||
| import {IssueSelection} from "../types/issue.ts"; | |||||
| import {ProposalSelection} from "../types/proposal.ts"; | |||||
| import {useLoading} from "../composables/useLoading.ts"; | import {useLoading} from "../composables/useLoading.ts"; | ||||
| import IssueModal from "./modals/IssueModal.vue"; | |||||
| import ProposalModal from "./modals/ProposalModal.vue"; | |||||
| import {ContentModal, useModal} from "../composables/useModal.ts"; | import {ContentModal, useModal} from "../composables/useModal.ts"; | ||||
| import FilterModal from "./modals/FilterModal.vue"; | import FilterModal from "./modals/FilterModal.vue"; | ||||
| import Filter from "./icons/Filter.vue"; | import Filter from "./icons/Filter.vue"; | ||||
| @@ -44,22 +44,22 @@ import {ref} from "vue"; | |||||
| import {ProposalList} from "../generated/typescript"; | import {ProposalList} from "../generated/typescript"; | ||||
| const { show: showModal } = useModal(); | const { show: showModal } = useModal(); | ||||
| const props = defineProps<{ issues: ProposalList | undefined }>(); | |||||
| const props = defineProps<{ proposals: ProposalList | undefined }>(); | |||||
| const emit = defineEmits(['update:selection']); | const emit = defineEmits(['update:selection']); | ||||
| const {isLoading} = useLoading(); | const {isLoading} = useLoading(); | ||||
| const selectedId = ref<number>(); | const selectedId = ref<number>(); | ||||
| const handleSelection = (issue: IssueSelection) => { | |||||
| const id = issue.proposal.id; | |||||
| const handleSelection = (proposal: ProposalSelection) => { | |||||
| const id = proposal.proposal.id; | |||||
| if (selectedId.value && selectedId.value === id) return; | if (selectedId.value && selectedId.value === id) return; | ||||
| selectedId.value = id; | selectedId.value = id; | ||||
| emit('update:selection', {proposal: issue.proposal, cid: issue.cid}); | |||||
| emit('update:selection', {proposal: proposal.proposal}); | |||||
| } | } | ||||
| const showNewIssueModal = () => { | |||||
| showModal(IssueModal as ContentModal); | |||||
| const showNewProposalModal = () => { | |||||
| showModal(ProposalModal as ContentModal); | |||||
| } | } | ||||
| const showFilterModal = () => { | const showFilterModal = () => { | ||||
| @@ -30,7 +30,7 @@ | |||||
| </template> | </template> | ||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||
| import {VoteIssueRequest, VoteIssueResponse} from "../types/issue.ts"; | |||||
| import {VoteProposalRequest, VoteProposalResponse} from "../types/proposal.ts"; | |||||
| import Cancel from "./icons/Cancel.vue"; | import Cancel from "./icons/Cancel.vue"; | ||||
| import Confirm from "./icons/Confirm.vue"; | import Confirm from "./icons/Confirm.vue"; | ||||
| import freighter from "@stellar/freighter-api"; | import freighter from "@stellar/freighter-api"; | ||||
| @@ -42,15 +42,15 @@ const recordAccept = async () => { | |||||
| if (!props.selectedId) return; | if (!props.selectedId) return; | ||||
| const addr = await freighter.getAddress(); | const addr = await freighter.getAddress(); | ||||
| // TODO: replace this call | // TODO: replace this call | ||||
| await post<VoteIssueRequest, VoteIssueResponse>('/issues/vote', | |||||
| { issue_id: props.selectedId.toString(), wallet: addr.address, vote: "Positive" }) | |||||
| await post<VoteProposalRequest, VoteProposalResponse>('/issues/vote', | |||||
| { proposal_id: props.selectedId.toString(), wallet: addr.address, vote: "Positive" }) | |||||
| } | } | ||||
| const recordDecline = async () => { | const recordDecline = async () => { | ||||
| if (!props.selectedId) return; | if (!props.selectedId) return; | ||||
| const addr = await freighter.getAddress(); | const addr = await freighter.getAddress(); | ||||
| // TODO: replace this call | // TODO: replace this call | ||||
| await post<VoteIssueRequest, VoteIssueResponse>('/issues/vote', | |||||
| { issue_id: props.selectedId, wallet: addr.address, vote: "Negative" }) | |||||
| await post<VoteProposalRequest, VoteProposalResponse>('/issues/vote', | |||||
| { proposal_id: props.selectedId, wallet: addr.address, vote: "Negative" }) | |||||
| } | } | ||||
| const recordAbstain = async () => { | const recordAbstain = async () => { | ||||
| if (!props.selectedId) return; | if (!props.selectedId) return; | ||||
| @@ -0,0 +1,31 @@ | |||||
| <script setup lang="ts"> | |||||
| </script> | |||||
| <template> | |||||
| <div class="h-4 w-4 cursor-pointer mx-2"> | |||||
| <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |||||
| viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve"> | |||||
| <g> | |||||
| <g> | |||||
| <path d="M28.8,7.1c-0.2-0.1-0.4-0.1-0.5,0C28.1,7.1,28,7.3,28,7.5V20c0,1.9-1.6,3.5-3.5,3.5H3c-0.2,0-0.4,0.1-0.4,0.3 | |||||
| c-0.1,0.2-0.1,0.4,0,0.5c0.6,0.8,1.4,1.2,2.4,1.2h22c1.7,0,3-1.3,3-3v-13C30,8.6,29.6,7.7,28.8,7.1z"/> | |||||
| </g> | |||||
| <g> | |||||
| <path d="M27,20V7c0-1.4-1.1-2.5-2.5-2.5h-22C1.1,4.5,0,5.6,0,7v13c0,1.4,1.1,2.5,2.5,2.5h22C25.9,22.5,27,21.4,27,20z M18.5,9.5h6 | |||||
| c0.3,0,0.5,0.2,0.5,0.5s-0.2,0.5-0.5,0.5h-6c-0.3,0-0.5-0.2-0.5-0.5S18.2,9.5,18.5,9.5z M18.5,13.5h6c0.3,0,0.5,0.2,0.5,0.5 | |||||
| s-0.2,0.5-0.5,0.5h-6c-0.3,0-0.5-0.2-0.5-0.5S18.2,13.5,18.5,13.5z M9,20c-3.6,0-6.5-2.9-6.5-6.5S5.4,7,9,7s6.5,2.9,6.5,6.5 | |||||
| S12.6,20,9,20z M18,18c0-0.3,0.2-0.5,0.5-0.5h6c0.3,0,0.5,0.2,0.5,0.5s-0.2,0.5-0.5,0.5h-6C18.2,18.5,18,18.3,18,18z"/> | |||||
| </g> | |||||
| <g> | |||||
| <path d="M10.9,14.8c0.7-0.6,1.1-1.4,1.1-2.3c0-1.7-1.3-3-3-3s-3,1.3-3,3c0,0.9,0.4,1.8,1.1,2.3c-1,0.3-1.9,1-2.7,1.8 | |||||
| C5.5,18,7.1,19,9,19s3.5-1,4.5-2.4C12.8,15.8,11.9,15.2,10.9,14.8z"/> | |||||
| </g> | |||||
| </g> | |||||
| </svg> | |||||
| </div> | |||||
| </template> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -0,0 +1,24 @@ | |||||
| <template> | |||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |||||
| :viewBox="`0 0 ${viewBox} ${viewBox}`" style="width: 20px; height: 20px;" xml:space="preserve" fill="white"> | |||||
| <path d="M0.271,10.444l14.5,7.5C14.842,17.981,14.921,18,15,18s0.158-0.019,0.229-0.056l14.5-7.5C29.896,10.358,30,10.187,30,10 | |||||
| s-0.104-0.358-0.271-0.444l-14.5-7.5c-0.144-0.074-0.315-0.074-0.459,0l-14.5,7.5C0.104,9.642,0,9.813,0,10 | |||||
| S0.104,10.358,0.271,10.444z"/> | |||||
| <path d="M29.729,19.556l-2.886-1.493l-11.155,5.77C15.477,23.942,15.239,24,15,24s-0.477-0.058-0.689-0.167l-11.155-5.77 | |||||
| l-2.886,1.493C0.104,19.642,0,19.813,0,20s0.104,0.358,0.271,0.444l14.5,7.5C14.842,27.981,14.921,28,15,28 | |||||
| s0.158-0.019,0.229-0.056l14.5-7.5C29.896,20.358,30,20.187,30,20S29.896,19.642,29.729,19.556z"/> | |||||
| <path d="M29.729,14.556l-2.886-1.493l-11.155,5.77C15.477,18.942,15.239,19,15,19s-0.477-0.058-0.689-0.167l-11.155-5.77 | |||||
| l-2.886,1.493C0.104,14.642,0,14.813,0,15s0.104,0.358,0.271,0.444l14.5,7.5C14.842,22.981,14.921,23,15,23 | |||||
| s0.158-0.019,0.229-0.056l14.5-7.5C29.896,15.358,30,15.187,30,15S29.896,14.642,29.729,14.556z"/> | |||||
| </svg> | |||||
| </template> | |||||
| <script setup lang="ts"> | |||||
| const props = defineProps({ | |||||
| viewBox: {type: Number, default: 90}, | |||||
| }) | |||||
| </script> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -20,7 +20,7 @@ | |||||
| <script setup lang="ts" generic="T extends ContentModal"> | <script setup lang="ts" generic="T extends ContentModal"> | ||||
| import Modal from "./Modal.vue"; | import Modal from "./Modal.vue"; | ||||
| import { ContentModal } from "../../composables/useModal.ts"; | |||||
| import { ContentModal } from "../../stores/modal.ts"; | |||||
| import { useFilterNotifier } from "../../composables/useFilterNotifier.ts"; | import { useFilterNotifier } from "../../composables/useFilterNotifier.ts"; | ||||
| import { eventBus } from "../../utils/eventBus.ts"; | import { eventBus } from "../../utils/eventBus.ts"; | ||||
| @@ -1,40 +0,0 @@ | |||||
| <template> | |||||
| <Modal header="New Issue" @on-closed="submit"> | |||||
| <slot> | |||||
| <div class="my-3"> | |||||
| <input type="text" class="w-full border-gray-300 border border-solid py-2 px-2.5" placeholder="Title" aria-label="Title" v-model="title" /> | |||||
| </div> | |||||
| <div class="my-3"> | |||||
| <PostBuilder @update:content="assignDescription" /> | |||||
| </div> | |||||
| </slot> | |||||
| </Modal> | |||||
| </template> | |||||
| <script setup lang="ts" generic="T extends ContentModal"> | |||||
| import Modal from "./Modal.vue"; | |||||
| import { ref } from "vue"; | |||||
| import PostBuilder from "../PostBuilder.vue"; | |||||
| import { ContentModal } from "../../composables/useModal.ts"; | |||||
| import freighter from "@stellar/freighter-api"; | |||||
| import {ProposalService} from "../../generated/typescript"; | |||||
| const title = ref(''); | |||||
| const description = ref<string>(''); | |||||
| const submit = async () => { | |||||
| if (await freighter.isAllowed()) { | |||||
| const addr = await freighter.getAddress(); | |||||
| await ProposalService.createProposal({creator: addr.address, name: title.value, description: description.value}); | |||||
| } | |||||
| } | |||||
| const assignDescription = (paragraphs: string) => { | |||||
| description.value = paragraphs; | |||||
| } | |||||
| </script> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -1,44 +1,144 @@ | |||||
| <template> | <template> | ||||
| <div ref="modalRef" class="fixed top-0 left-0 w-screen h-screen p-4 sm:p-40" style="background-color:rgba(0, 0, 0, 0.33)" @click.self="hide" v-if="getVisibility()"> | |||||
| <div class="h-full max-h-[500px] bg-white rounded-xl p-6 mx-auto max-w-[540px]"> | |||||
| <div class="flex flex-row justify-end" @click="hide"> | |||||
| <span class="text-2xl text-gray-600 leading-3 cursor-pointer">×</span> | |||||
| </div> | |||||
| <div class="flex-1 w-full overflow-y-auto"> | |||||
| <div> | |||||
| <div> | |||||
| <span class="text-2xl mx-auto font-bold"> | |||||
| <div> | |||||
| <Teleport to="body"> | |||||
| <Transition | |||||
| enter-active-class="transition duration-200 ease-out" | |||||
| enter-from-class="opacity-0" | |||||
| enter-to-class="opacity-100" | |||||
| leave-active-class="transition duration-150 ease-in" | |||||
| leave-from-class="opacity-100" | |||||
| leave-to-class="opacity-0" | |||||
| > | |||||
| <div | |||||
| v-if="isOpen" | |||||
| ref="modalRef" | |||||
| class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/45 p-4 sm:p-6" | |||||
| role="presentation" | |||||
| @click.self="handleBackdropClick" | |||||
| @keydown="handleKeydown" | |||||
| > | |||||
| <section | |||||
| class="flex max-h-[min(720px,calc(100vh-2rem))] w-full max-w-2xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5" | |||||
| role="dialog" | |||||
| aria-modal="true" | |||||
| :aria-label="header" | |||||
| > | |||||
| <header class="flex items-center justify-between border-b border-slate-200 px-5 py-4 sm:px-6"> | |||||
| <h2 class="text-xl font-semibold tracking-tight text-slate-900"> | |||||
| {{ header }} | {{ header }} | ||||
| </span> | |||||
| </div> | |||||
| </h2> | |||||
| <button | |||||
| ref="closeButtonRef" | |||||
| type="button" | |||||
| class="inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 focus:outline-none focus:ring-4 focus:ring-amber-500/20" | |||||
| aria-label="Close modal" | |||||
| @click="close" | |||||
| > | |||||
| <span aria-hidden="true" class="text-2xl leading-none">×</span> | |||||
| </button> | |||||
| </header> | |||||
| <main class="min-h-0 flex-1 overflow-y-auto px-5 py-5 sm:px-6"> | |||||
| <slot /> | |||||
| </main> | |||||
| <footer class="flex flex-col-reverse gap-3 border-t border-slate-200 bg-slate-50 px-5 py-4 sm:flex-row sm:justify-end sm:px-6"> | |||||
| <button | |||||
| type="button" | |||||
| class="rounded-lg px-4 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-200 focus:outline-none focus:ring-4 focus:ring-slate-300" | |||||
| @click="close" | |||||
| > | |||||
| Cancel | |||||
| </button> | |||||
| <button | |||||
| type="button" | |||||
| class="rounded-lg bg-amber-500 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-4 focus:ring-amber-500/30 disabled:cursor-not-allowed disabled:opacity-50" | |||||
| :disabled="submitDisabled" | |||||
| @click="handleSubmission" | |||||
| > | |||||
| {{ buttonText }} | |||||
| </button> | |||||
| </footer> | |||||
| </section> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="h-2/3 overflow-y-auto border border-gray-200 rounded-md px-2"> | |||||
| <slot name="default" /> | |||||
| </div> | |||||
| <div class="flex flex-row justify-end pt-2"> | |||||
| <button class="bg-amber-500 rounded py-2 px-3 text-white font-bold" @click="handleSubmission"> | |||||
| {{ buttonText }} | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| </Transition> | |||||
| </Teleport> | |||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||
| import { useModal } from "../../composables/useModal.ts"; | |||||
| import { withDefaults } from "vue"; | |||||
| import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue"; | |||||
| import {useModal} from "../../composables/useModal.ts"; | |||||
| const { hide, getVisibility } = useModal(); | |||||
| const props = withDefaults( | |||||
| defineProps<{ | |||||
| modelValue: boolean; | |||||
| header: string; | |||||
| buttonText?: string; | |||||
| submitDisabled?: boolean; | |||||
| closeOnBackdrop?: boolean; | |||||
| }>(), | |||||
| { | |||||
| buttonText: "Submit", | |||||
| submitDisabled: false, | |||||
| closeOnBackdrop: true, | |||||
| }, | |||||
| ); | |||||
| withDefaults(defineProps<{ header: string; buttonText?: string }>(), { | |||||
| buttonText: 'Submit' | |||||
| }); | |||||
| const emits = defineEmits(['onClosed']); | |||||
| const handleSubmission = () => { | |||||
| emits('onClosed'); | |||||
| const emit = defineEmits<{ | |||||
| "update:modelValue": [value: boolean]; | |||||
| submit: []; | |||||
| }>(); | |||||
| const {hide} = useModal(); | |||||
| const modalRef = ref<HTMLElement | null>(null); | |||||
| const closeButtonRef = ref<HTMLButtonElement | null>(null); | |||||
| const isOpen = computed(() => props.modelValue); | |||||
| function close() { | |||||
| emit("update:modelValue", false); | |||||
| hide(); | hide(); | ||||
| }; | |||||
| } | |||||
| function handleBackdropClick() { | |||||
| if (props.closeOnBackdrop) { | |||||
| close(); | |||||
| } | |||||
| } | |||||
| function handleSubmission() { | |||||
| if (!props.submitDisabled) { | |||||
| emit("submit"); | |||||
| } | |||||
| } | |||||
| function handleKeydown(event: KeyboardEvent) { | |||||
| if (event.key === "Escape") { | |||||
| close(); | |||||
| } | |||||
| } | |||||
| watch( | |||||
| isOpen, | |||||
| async (open) => { | |||||
| if (open) { | |||||
| document.body.style.overflow = "hidden"; | |||||
| await nextTick(); | |||||
| closeButtonRef.value?.focus(); | |||||
| } else { | |||||
| document.body.style.overflow = ""; | |||||
| } | |||||
| }, | |||||
| { immediate: true }, | |||||
| ); | |||||
| onBeforeUnmount(() => { | |||||
| document.body.style.overflow = ""; | |||||
| }); | |||||
| </script> | </script> | ||||
| <style scoped> | <style scoped> | ||||
| @@ -0,0 +1,74 @@ | |||||
| <template> | |||||
| <Modal | |||||
| v-model="isNewProposalOpen" | |||||
| header="New Proposal" | |||||
| button-text="Create proposal" | |||||
| :submit-disabled="!title.trim()" | |||||
| @submit="submit" | |||||
| > | |||||
| <div class="space-y-5"> | |||||
| <div> | |||||
| <label for="proposal-title" class="mb-1.5 block text-sm font-medium text-slate-700"> | |||||
| Title | |||||
| </label> | |||||
| <input | |||||
| id="proposal-title" | |||||
| v-model="title" | |||||
| type="text" | |||||
| class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-slate-900 outline-none transition placeholder:text-slate-400 focus:border-amber-500 focus:ring-4 focus:ring-amber-500/15" | |||||
| placeholder="Briefly describe the proposal" | |||||
| aria-describedby="proposal-title-help" | |||||
| @keydown.meta.enter="submit" | |||||
| @keydown.ctrl.enter="submit" | |||||
| /> | |||||
| <p id="proposal-title-help" class="mt-1.5 text-xs text-slate-500"> | |||||
| Keep it concise; add supporting detail below. | |||||
| </p> | |||||
| </div> | |||||
| <div> | |||||
| <label class="mb-1.5 block text-sm font-medium text-slate-700"> | |||||
| Description | |||||
| </label> | |||||
| <PostBuilder @update:content="assignDescription" /> | |||||
| </div> | |||||
| </div> | |||||
| </Modal> | |||||
| </template> | |||||
| <script setup lang="ts" generic="T extends ContentModal"> | |||||
| import Modal from "./Modal.vue"; | |||||
| import { ref } from "vue"; | |||||
| import PostBuilder from "../PostBuilder.vue"; | |||||
| import { ContentModal } from "../../composables/useModal.ts"; | |||||
| import freighter from "@stellar/freighter-api"; | |||||
| import {ProposalService} from "../../generated/typescript"; | |||||
| import {useModal} from "../../composables/useModal.ts"; | |||||
| import {ProposalParagraph} from "../../types/proposal.ts"; | |||||
| const {getVisibility} = useModal(); | |||||
| const title = ref(''); | |||||
| const description = ref<ProposalParagraph[]>([]); | |||||
| const submit = async () => { | |||||
| if (await freighter.isAllowed()) { | |||||
| const addr = await freighter.getAddress(); | |||||
| await ProposalService.createProposal({creator: addr.address, name: title.value, paragraphs: description.value}); | |||||
| } | |||||
| } | |||||
| const assignDescription = (paragraphs: ProposalParagraph[]) => { | |||||
| description.value = paragraphs; | |||||
| } | |||||
| const isNewProposalOpen = ref(getVisibility()); | |||||
| </script> | |||||
| <style scoped> | |||||
| </style> | |||||
| @@ -1,71 +0,0 @@ | |||||
| /** | |||||
| * @deprecated This method is deprecated. | |||||
| */ | |||||
| export const useFetch = () => { | |||||
| interface FetchConfig { | |||||
| baseUrl: string; | |||||
| headers: HeadersInit; | |||||
| } | |||||
| interface ApiResponse<T> { | |||||
| data: T; | |||||
| status: number; | |||||
| } | |||||
| const DEFAULT_CONFIG: FetchConfig = { | |||||
| baseUrl: '/api/v1', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json' | |||||
| } | |||||
| }; | |||||
| const buildQueryString = (params?: Record<string, string | number>): string => { | |||||
| if (!params) return ''; | |||||
| return '?' + Object.entries(params) | |||||
| .map(([key, value]) => `${key}=${value}`) | |||||
| .join('&'); | |||||
| }; | |||||
| /** | |||||
| * @deprecated This function is deprecated. | |||||
| */ | |||||
| const createRequest = async <T>( | |||||
| endpoint: string, | |||||
| method: 'GET' | 'POST', | |||||
| options: RequestInit = {} | |||||
| ): Promise<ApiResponse<T>> => { | |||||
| const response = await fetch(`${DEFAULT_CONFIG.baseUrl}${endpoint}`, { | |||||
| method, | |||||
| mode: 'cors', | |||||
| headers: new Headers(DEFAULT_CONFIG.headers), | |||||
| ...options | |||||
| }); | |||||
| const data = await response.json(); | |||||
| return {data, status: response.status}; | |||||
| }; | |||||
| /** | |||||
| * @deprecated This function is deprecated. | |||||
| */ | |||||
| const get = async <T>( | |||||
| endpoint: string, | |||||
| queryParams?: Record<string, string | number> | |||||
| ): Promise<T> => { | |||||
| const query = buildQueryString(queryParams); | |||||
| const response = await createRequest<T>(`${endpoint}${query}`, 'GET'); | |||||
| return response.data; | |||||
| }; | |||||
| /** | |||||
| * @deprecated This function is deprecated. | |||||
| */ | |||||
| const post = async <T, U>(endpoint: string, payload: T): Promise<U> => { | |||||
| const response = await createRequest<U>(endpoint, 'POST', { | |||||
| body: JSON.stringify(payload) | |||||
| }); | |||||
| return response.data; | |||||
| }; | |||||
| return { get, post } | |||||
| } | |||||
| @@ -20,8 +20,8 @@ export type OpenAPIConfig = { | |||||
| }; | }; | ||||
| export const OpenAPI: OpenAPIConfig = { | export const OpenAPI: OpenAPIConfig = { | ||||
| BASE: 'http://localhost:7300', | |||||
| VERSION: '1.0.0', | |||||
| BASE: '', | |||||
| VERSION: '0.1.0', | |||||
| WITH_CREDENTIALS: false, | WITH_CREDENTIALS: false, | ||||
| CREDENTIALS: 'include', | CREDENTIALS: 'include', | ||||
| TOKEN: undefined, | TOKEN: undefined, | ||||
| @@ -7,23 +7,17 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; | |||||
| export { OpenAPI } from './core/OpenAPI'; | export { OpenAPI } from './core/OpenAPI'; | ||||
| export type { OpenAPIConfig } from './core/OpenAPI'; | export type { OpenAPIConfig } from './core/OpenAPI'; | ||||
| export type { AddAmendmentRequest } from './models/AddAmendmentRequest'; | |||||
| export type { AddProposalRequest } from './models/AddProposalRequest'; | export type { AddProposalRequest } from './models/AddProposalRequest'; | ||||
| export type { FreighterLoginRequest } from './models/FreighterLoginRequest'; | export type { FreighterLoginRequest } from './models/FreighterLoginRequest'; | ||||
| export type { ListProposalsResponse } from './models/ListProposalsResponse'; | |||||
| export type { LoginRequest } from './models/LoginRequest'; | export type { LoginRequest } from './models/LoginRequest'; | ||||
| export type { ProposalItem } from './models/ProposalItem'; | |||||
| export type { ProposalList } from './models/ProposalList'; | |||||
| export type { Proposal } from './models/Proposal'; | |||||
| export type { ProposalParagraph } from './models/ProposalParagraph'; | |||||
| export type { ProposalParagraphRequest } from './models/ProposalParagraphRequest'; | |||||
| export type { ProposalWithParagraphs } from './models/ProposalWithParagraphs'; | |||||
| export type { ProposalWithSummary } from './models/ProposalWithSummary'; | |||||
| export type { RegisterRequest } from './models/RegisterRequest'; | export type { RegisterRequest } from './models/RegisterRequest'; | ||||
| export type { VisitProposalRequest } from './models/VisitProposalRequest'; | |||||
| export { $AddProposalRequest } from './schemas/$AddProposalRequest'; | |||||
| export { $FreighterLoginRequest } from './schemas/$FreighterLoginRequest'; | |||||
| export { $ListProposalsResponse } from './schemas/$ListProposalsResponse'; | |||||
| export { $LoginRequest } from './schemas/$LoginRequest'; | |||||
| export { $ProposalItem } from './schemas/$ProposalItem'; | |||||
| export { $ProposalList } from './schemas/$ProposalList'; | |||||
| export { $RegisterRequest } from './schemas/$RegisterRequest'; | |||||
| export { $VisitProposalRequest } from './schemas/$VisitProposalRequest'; | |||||
| export type { SelectableProposal } from './models/SelectableProposal'; | |||||
| export { AmendmentService } from './services/AmendmentService'; | export { AmendmentService } from './services/AmendmentService'; | ||||
| export { AuthService } from './services/AuthService'; | export { AuthService } from './services/AuthService'; | ||||
| @@ -2,8 +2,10 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| import type { ProposalList } from './ProposalList'; | |||||
| export type ListProposalsResponse = { | |||||
| proposals?: ProposalList; | |||||
| export type AddAmendmentRequest = { | |||||
| content: string; | |||||
| creator: string; | |||||
| name: string; | |||||
| proposalId: number; | |||||
| }; | }; | ||||
| @@ -2,9 +2,10 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| import type { ProposalParagraphRequest } from './ProposalParagraphRequest'; | |||||
| export type AddProposalRequest = { | export type AddProposalRequest = { | ||||
| creator: string; | creator: string; | ||||
| description: string; | |||||
| name: string; | name: string; | ||||
| paragraphs: Array<ProposalParagraphRequest>; | |||||
| }; | }; | ||||
| @@ -0,0 +1,11 @@ | |||||
| /* generated using openapi-typescript-codegen -- do not edit */ | |||||
| /* istanbul ignore file */ | |||||
| /* tslint:disable */ | |||||
| /* eslint-disable */ | |||||
| export type Proposal = { | |||||
| createdAt: string; | |||||
| creator: string; | |||||
| name: string; | |||||
| updatedAt: string; | |||||
| }; | |||||
| @@ -2,12 +2,9 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| export type VisitProposalRequest = { | |||||
| method?: string; | |||||
| path?: string; | |||||
| export type ProposalParagraph = { | |||||
| index: number; | |||||
| paragraphText: string; | |||||
| proposalId: number; | proposalId: number; | ||||
| queryString?: string; | |||||
| referrer?: string; | |||||
| statusCode?: number; | |||||
| }; | }; | ||||
| @@ -2,5 +2,8 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| import type { ProposalItem } from './ProposalItem'; | |||||
| export type ProposalList = Array<ProposalItem>; | |||||
| export type ProposalParagraphRequest = { | |||||
| index: number; | |||||
| paragraphText: string; | |||||
| }; | |||||
| @@ -0,0 +1,14 @@ | |||||
| /* generated using openapi-typescript-codegen -- do not edit */ | |||||
| /* istanbul ignore file */ | |||||
| /* tslint:disable */ | |||||
| /* eslint-disable */ | |||||
| import type { ProposalParagraph } from './ProposalParagraph'; | |||||
| export type ProposalWithParagraphs = { | |||||
| createdAt: string; | |||||
| creator: string; | |||||
| id: number; | |||||
| name: string; | |||||
| paragraphs: Array<ProposalParagraph>; | |||||
| updatedAt?: string | null; | |||||
| }; | |||||
| @@ -2,15 +2,12 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| export type ProposalItem = { | |||||
| cid: string; | |||||
| createdAt?: string | null; | |||||
| export type ProposalWithSummary = { | |||||
| createdAt: string; | |||||
| creator: string; | creator: string; | ||||
| id: number; | id: number; | ||||
| isCurrent: boolean; | |||||
| name: string; | name: string; | ||||
| previousCid?: string | null; | |||||
| summary?: string | null; | |||||
| summary: string; | |||||
| updatedAt?: string | null; | updatedAt?: string | null; | ||||
| }; | }; | ||||
| @@ -0,0 +1,12 @@ | |||||
| /* generated using openapi-typescript-codegen -- do not edit */ | |||||
| /* istanbul ignore file */ | |||||
| /* tslint:disable */ | |||||
| /* eslint-disable */ | |||||
| export type SelectableProposal = { | |||||
| createdAt: string; | |||||
| creator: string; | |||||
| id: number; | |||||
| name: string; | |||||
| updatedAt?: string | null; | |||||
| }; | |||||
| @@ -2,57 +2,54 @@ | |||||
| /* istanbul ignore file */ | /* istanbul ignore file */ | ||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| import type { AddAmendmentRequest } from '../models/AddAmendmentRequest'; | |||||
| import type { CancelablePromise } from '../core/CancelablePromise'; | import type { CancelablePromise } from '../core/CancelablePromise'; | ||||
| import { OpenAPI } from '../core/OpenAPI'; | import { OpenAPI } from '../core/OpenAPI'; | ||||
| import { request as __request } from '../core/request'; | import { request as __request } from '../core/request'; | ||||
| export class AmendmentService { | export class AmendmentService { | ||||
| /** | /** | ||||
| * Create an amendment | |||||
| * @param requestBody | * @param requestBody | ||||
| * @returns any OK | |||||
| * @returns any CID of the added amendment | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static createAmendment( | |||||
| requestBody: Record<string, any>, | |||||
| public static addAmendment( | |||||
| requestBody: AddAmendmentRequest, | |||||
| ): CancelablePromise<any> { | ): CancelablePromise<any> { | ||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'POST', | method: 'POST', | ||||
| url: '/api/v1/amendment/add', | url: '/api/v1/amendment/add', | ||||
| body: requestBody, | body: requestBody, | ||||
| mediaType: 'application/json', | mediaType: 'application/json', | ||||
| errors: { | |||||
| 400: `Bad request`, | |||||
| 500: `Internal server error`, | |||||
| }, | |||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Get an amendment | |||||
| * @param id | |||||
| * @returns any OK | |||||
| * @returns any Amendment details | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static getAmendment( | |||||
| id: number, | |||||
| ): CancelablePromise<Record<string, any>> { | |||||
| public static getAmendment(): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'GET', | method: 'GET', | ||||
| url: '/api/v1/amendment/get', | url: '/api/v1/amendment/get', | ||||
| query: { | |||||
| 'id': id, | |||||
| errors: { | |||||
| 400: `Bad request`, | |||||
| 500: `Internal server error`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * List amendments | |||||
| * @param proposalId | |||||
| * @returns any OK | |||||
| * @returns any List of amendments for a proposal | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static listAmendments( | |||||
| proposalId: number, | |||||
| ): CancelablePromise<Record<string, any>> { | |||||
| public static listAmendments(): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'GET', | method: 'GET', | ||||
| url: '/api/v1/amendments/list', | url: '/api/v1/amendments/list', | ||||
| query: { | |||||
| 'proposalId': proposalId, | |||||
| errors: { | |||||
| 500: `Internal server error`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -10,71 +10,76 @@ import { OpenAPI } from '../core/OpenAPI'; | |||||
| import { request as __request } from '../core/request'; | import { request as __request } from '../core/request'; | ||||
| export class AuthService { | export class AuthService { | ||||
| /** | /** | ||||
| * Login with username/password | |||||
| * @param requestBody | * @param requestBody | ||||
| * @returns any OK | |||||
| * @returns any User logged in successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static loginUser( | |||||
| public static login( | |||||
| requestBody: LoginRequest, | requestBody: LoginRequest, | ||||
| ): CancelablePromise<Record<string, any>> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'POST', | method: 'POST', | ||||
| url: '/api/v1/auth/login', | url: '/api/v1/auth/login', | ||||
| body: requestBody, | body: requestBody, | ||||
| mediaType: 'application/json', | mediaType: 'application/json', | ||||
| errors: { | errors: { | ||||
| 400: `Bad request`, | |||||
| 401: `Unauthorized`, | 401: `Unauthorized`, | ||||
| 500: `Internal server error`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Login with Stellar (Freighter) | |||||
| * @param requestBody | * @param requestBody | ||||
| * @returns any OK | |||||
| * @returns any User logged in successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static loginFreighter( | public static loginFreighter( | ||||
| requestBody: FreighterLoginRequest, | requestBody: FreighterLoginRequest, | ||||
| ): CancelablePromise<Record<string, any>> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'POST', | method: 'POST', | ||||
| url: '/api/v1/auth/login/freighter', | url: '/api/v1/auth/login/freighter', | ||||
| body: requestBody, | body: requestBody, | ||||
| mediaType: 'application/json', | mediaType: 'application/json', | ||||
| errors: { | errors: { | ||||
| 400: `Bad request`, | |||||
| 401: `Unauthorized`, | 401: `Unauthorized`, | ||||
| 500: `Internal server error`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Get freighter login nonce | |||||
| * @returns any OK | |||||
| * @returns any User logged in successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static getFreighterNonce(): CancelablePromise<Record<string, any>> { | |||||
| public static getNonce(): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'GET', | |||||
| method: 'POST', | |||||
| url: '/api/v1/auth/nonce', | url: '/api/v1/auth/nonce', | ||||
| errors: { | |||||
| 400: `Bad request`, | |||||
| 401: `Unauthorized`, | |||||
| 500: `Internal server error`, | |||||
| }, | |||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Register a user | |||||
| * @param requestBody | * @param requestBody | ||||
| * @returns any Created | |||||
| * @returns any User registered successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static registerUser( | |||||
| public static register( | |||||
| requestBody: RegisterRequest, | requestBody: RegisterRequest, | ||||
| ): CancelablePromise<Record<string, any>> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'POST', | method: 'POST', | ||||
| url: '/api/v1/auth/register', | url: '/api/v1/auth/register', | ||||
| body: requestBody, | body: requestBody, | ||||
| mediaType: 'application/json', | mediaType: 'application/json', | ||||
| errors: { | errors: { | ||||
| 400: `Bad Request`, | |||||
| 409: `Conflict`, | |||||
| 400: `Bad request`, | |||||
| 500: `Internal server error`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -3,75 +3,59 @@ | |||||
| /* tslint:disable */ | /* tslint:disable */ | ||||
| /* eslint-disable */ | /* eslint-disable */ | ||||
| import type { AddProposalRequest } from '../models/AddProposalRequest'; | import type { AddProposalRequest } from '../models/AddProposalRequest'; | ||||
| import type { ListProposalsResponse } from '../models/ListProposalsResponse'; | |||||
| import type { VisitProposalRequest } from '../models/VisitProposalRequest'; | |||||
| import type { CancelablePromise } from '../core/CancelablePromise'; | import type { CancelablePromise } from '../core/CancelablePromise'; | ||||
| import { OpenAPI } from '../core/OpenAPI'; | import { OpenAPI } from '../core/OpenAPI'; | ||||
| import { request as __request } from '../core/request'; | import { request as __request } from '../core/request'; | ||||
| export class ProposalService { | export class ProposalService { | ||||
| /** | /** | ||||
| * Create a proposal | |||||
| * @param requestBody | * @param requestBody | ||||
| * @returns any OK | |||||
| * @returns any Proposal added successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static createProposal( | |||||
| public static addProposal( | |||||
| requestBody: AddProposalRequest, | requestBody: AddProposalRequest, | ||||
| ): CancelablePromise<Record<string, any>> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'POST', | method: 'POST', | ||||
| url: '/api/v1/proposal/add', | url: '/api/v1/proposal/add', | ||||
| body: requestBody, | body: requestBody, | ||||
| mediaType: 'application/json', | mediaType: 'application/json', | ||||
| errors: { | errors: { | ||||
| 400: `Bad Request`, | |||||
| 400: `Bad request`, | |||||
| 500: `Failed to add proposal`, | |||||
| }, | }, | ||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Get a proposal by id | |||||
| * @param id | * @param id | ||||
| * @returns any OK | |||||
| * @returns any Proposal retrieved successfully | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static getProposal( | public static getProposal( | ||||
| id: number, | id: number, | ||||
| ): CancelablePromise<Record<string, any>> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'GET', | method: 'GET', | ||||
| url: '/api/v1/proposal/get', | url: '/api/v1/proposal/get', | ||||
| query: { | query: { | ||||
| 'id': id, | 'id': id, | ||||
| }, | }, | ||||
| errors: { | |||||
| 400: `Invalid proposalId`, | |||||
| 500: `Failed to retrieve proposal`, | |||||
| }, | |||||
| }); | }); | ||||
| } | } | ||||
| /** | /** | ||||
| * Record a proposal visit | |||||
| * @param requestBody | |||||
| * @returns any Created | |||||
| * @throws ApiError | |||||
| */ | |||||
| public static recordProposalVisit( | |||||
| requestBody: VisitProposalRequest, | |||||
| ): CancelablePromise<Record<string, any>> { | |||||
| return __request(OpenAPI, { | |||||
| method: 'POST', | |||||
| url: '/api/v1/proposal/visit', | |||||
| body: requestBody, | |||||
| mediaType: 'application/json', | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * List proposals | |||||
| * @param offset | * @param offset | ||||
| * @param limit | * @param limit | ||||
| * @returns ListProposalsResponse OK | |||||
| * @returns any Returns a list of proposals | |||||
| * @throws ApiError | * @throws ApiError | ||||
| */ | */ | ||||
| public static listProposals( | public static listProposals( | ||||
| offset: number, | offset: number, | ||||
| limit: number, | limit: number, | ||||
| ): CancelablePromise<ListProposalsResponse> { | |||||
| ): CancelablePromise<any> { | |||||
| return __request(OpenAPI, { | return __request(OpenAPI, { | ||||
| method: 'GET', | method: 'GET', | ||||
| url: '/api/v1/proposals/list', | url: '/api/v1/proposals/list', | ||||
| @@ -79,6 +63,9 @@ export class ProposalService { | |||||
| 'offset': offset, | 'offset': offset, | ||||
| 'limit': limit, | 'limit': limit, | ||||
| }, | }, | ||||
| errors: { | |||||
| 500: `Internal server error`, | |||||
| }, | |||||
| }); | }); | ||||
| } | } | ||||
| } | } | ||||
| @@ -4,6 +4,10 @@ import App from './App.vue' | |||||
| import router from './router' | import router from './router' | ||||
| import { createPinia } from 'pinia' | import { createPinia } from 'pinia' | ||||
| import './index.css' | import './index.css' | ||||
| import { OpenAPI } from './generated/typescript' | |||||
| OpenAPI.BASE = '' | |||||
| OpenAPI.HEADERS = localStorage.getItem("token") ? {"Authorization": "Bearer " + localStorage.getItem("token")} : {}; | |||||
| const pinia = createPinia(); | const pinia = createPinia(); | ||||
| createApp(App).use(pinia).use(router).mount('#app') | createApp(App).use(pinia).use(router).mount('#app') | ||||
| @@ -1,5 +1,5 @@ | |||||
| // Base interface for common issue properties | // Base interface for common issue properties | ||||
| export interface BaseIssue { | |||||
| export interface BaseProposal { | |||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| cid: string; | cid: string; | ||||
| @@ -7,7 +7,7 @@ export interface BaseIssue { | |||||
| } | } | ||||
| // Main proposal issue interface extending base | // Main proposal issue interface extending base | ||||
| export interface ProposalIssue extends BaseIssue { | |||||
| export interface Proposal extends BaseProposal { | |||||
| summary: string; | summary: string; | ||||
| isCurrent: boolean; | isCurrent: boolean; | ||||
| previousCid?: string; | previousCid?: string; | ||||
| @@ -21,39 +21,43 @@ interface ContentHolder { | |||||
| } | } | ||||
| // Type for issue content combining base fields and content | // Type for issue content combining base fields and content | ||||
| export type ProposalContent = BaseIssue & ContentHolder; | |||||
| export type ProposalContent = BaseProposal & ContentHolder; | |||||
| export interface ListIssuesResponse { | |||||
| proposals: ProposalIssue[] | |||||
| export interface ListProposalsResponse { | |||||
| proposals: Proposal[] | |||||
| } | } | ||||
| export interface GetIssueResponse { | |||||
| export interface GetProposalResponse { | |||||
| proposal: ProposalContent; | proposal: ProposalContent; | ||||
| } | } | ||||
| export interface AddIssueRequest { | |||||
| export interface AddProposalRequest { | |||||
| name: string; | name: string; | ||||
| description: string; | description: string; | ||||
| creator: string; | creator: string; | ||||
| } | } | ||||
| export interface AddIssueResponse { | |||||
| export interface AddProposalResponse { | |||||
| cid: string; | cid: string; | ||||
| } | } | ||||
| export interface VoteIssueRequest { | |||||
| issue_id: string; | |||||
| export interface VoteProposalRequest { | |||||
| proposal_id: string; | |||||
| wallet: string; | wallet: string; | ||||
| vote: 'Positive' | 'Negative'; | vote: 'Positive' | 'Negative'; | ||||
| } | } | ||||
| export interface VoteIssueResponse { | |||||
| issue_id: string; | |||||
| export interface VoteProposalResponse { | |||||
| proposal_id: string; | |||||
| equity: number; | equity: number; | ||||
| positive: boolean; | positive: boolean; | ||||
| } | } | ||||
| export interface IssueSelection { | |||||
| export interface ProposalSelection { | |||||
| proposal: ProposalContent; | proposal: ProposalContent; | ||||
| cid: string; | |||||
| } | |||||
| export interface ProposalParagraph { | |||||
| paragraphText: string; | |||||
| index: number; | |||||
| } | } | ||||
| @@ -1,19 +1,12 @@ | |||||
| <template> | <template> | ||||
| <div class="home h-screen"> | |||||
| <div class="home h-screen pb-12 md:pb-0"> | |||||
| <div class="flex h-full"> | <div class="flex h-full"> | ||||
| <!-- Sidebar for md+ screens (always visible) --> | <!-- Sidebar for md+ screens (always visible) --> | ||||
| <Sidebar | <Sidebar | ||||
| :issues="issues" | |||||
| @update:selection="handleSelection" | |||||
| class="hidden md:block w-64 h-full overflow-y-auto flex-shrink-0 border-r" | |||||
| /> | |||||
| <!-- Slide-in Sidebar for small screens --> | |||||
| <Sidebar | |||||
| :issues="issues" | |||||
| @update:selection="handleSelection" | |||||
| class="md:hidden fixed inset-y-0 left-0 z-30 w-64 h-full overflow-y-auto border-r transform transition-transform duration-300 ease-in-out" | |||||
| :class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full'" | |||||
| v-show="width > 768 || isSidebarOpen" | |||||
| :proposals="proposals" | |||||
| @update:selection="handleSelection" | |||||
| class="fixed md:static w-64 h-full overflow-y-auto border-r md:flex-shrink-0 inset-y-0 left-0 z-30 transform transition-transform duration-300 ease-in-out" | |||||
| /> | /> | ||||
| <!-- Overlay for small screens when sidebar is open --> | <!-- Overlay for small screens when sidebar is open --> | ||||
| @@ -24,71 +17,248 @@ | |||||
| ></div> | ></div> | ||||
| <!-- Main Content --> | <!-- Main Content --> | ||||
| <main class="h-full flex-1 overflow-y-auto"> | |||||
| <template v-if="selected?.cid && !isLoading('content')"> | |||||
| <article class="px-5 pt-5 pb-12 min-h-0 h-full"> | |||||
| <header v-if="selected?.proposal.content" class="mb-6"> | |||||
| <h1 class="decoration-[#f7d012] text-5xl font-bold font-header underline underline-offset-4 mb-4"> | |||||
| {{ selected?.proposal.name }} | |||||
| </h1> | |||||
| <h2 class="font-header font-bold ml-2 text-2xl tracking-tight mb-3">Proposal</h2> | |||||
| <div class="content-body"> | |||||
| <p | |||||
| v-for="paragraph in paragraphs" | |||||
| class="my-3 text-mxs" | |||||
| v-html="applyFormatting(paragraph)" | |||||
| /> | |||||
| </div> | |||||
| <div class="inset-divider" aria-hidden="true"></div> | |||||
| <div class="flex flex-row mt-8"> | |||||
| <button class="flex flex-row items-center font-poppins rounded-full bg-gray-400 hover:bg-green-500 text-white px-1.5 py-0.5 mr-2 cursor-pointer"> | |||||
| <Confirm class="inline-block fill-white" :width="14" :height="14" :box="96" /> | |||||
| <span class="ml-1 font-poppins text-xs">Approve</span> | |||||
| </button> | |||||
| <button class="flex flex-row items-center font-poppins rounded-full bg-gray-400 hover:bg-red-500 text-white px-1.5 py-0.5 mr-2 cursor-pointer"> | |||||
| <Cancel class="inline-block fill-white" :width="14" :height="14" :box="96" /> | |||||
| <span class="ml-1 font-poppins text-xs">Reject</span> | |||||
| </button> | |||||
| <button class="flex flex-row items-center font-poppins rounded-full bg-gray-400 hover:bg-amber-500 text-white px-1.5 py-0.5 cursor-pointer"> | |||||
| <Abstain class="inline-block fill-white" :width="14" :height="14" :box="96" /> | |||||
| <span class="ml-1 font-poppins text-xs">Abstain</span> | |||||
| </button> | |||||
| <!-- <VotingControls class="w-full max-w-[480px] py-2 px-5 h-11 rounded-md bg-gray-600 border border-black" :selected-id="selected?.cid" :vote="selected.vote"/>--> | |||||
| <main class="h-full flex-1 overflow-y-auto bg-slate-100"> | |||||
| <template v-if="selected && !isLoading('content')"> | |||||
| <article | |||||
| v-if="selected.proposal" | |||||
| class="mx-auto w-full max-w-[96rem] px-5 py-6 sm:px-8 lg:px-10 lg:py-10" | |||||
| :aria-labelledby="`proposal-${selected.proposal.id}-title`" | |||||
| > | |||||
| <div class="grid gap-8 xl:grid-cols-[minmax(0,1fr)_15rem] xl:items-start"> | |||||
| <!-- Primary reading column --> | |||||
| <div class="min-w-0"> | |||||
| <header class="border-b border-slate-300 pb-7"> | |||||
| <div class="mb-4 flex flex-wrap items-center gap-2"> | |||||
| <span | |||||
| class="rounded-full border border-slate-300 bg-white px-2.5 py-1 | |||||
| text-[0.65rem] font-bold uppercase tracking-[0.12em] text-slate-600" | |||||
| > | |||||
| Proposal | |||||
| </span> | |||||
| <span | |||||
| class="rounded-full border border-slate-300 bg-slate-200/70 px-2.5 py-1 | |||||
| text-[0.65rem] font-bold uppercase tracking-[0.12em] text-slate-600" | |||||
| > | |||||
| {{ truncateWallet(selected.proposal.creator) }} | |||||
| </span> | |||||
| </div> | |||||
| <h1 | |||||
| :id="`proposal-${selected.proposal.id}-title`" | |||||
| class="max-w-4xl font-header text-4xl font-bold leading-[0.95] | |||||
| tracking-tight text-slate-950 sm:text-5xl" | |||||
| > | |||||
| {{ selected.proposal.name }} | |||||
| </h1> | |||||
| <div class="mt-5 flex min-w-0 items-center gap-2 text-xs text-slate-600"> | |||||
| <Identification | |||||
| class="size-4 shrink-0 text-slate-700" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| <span class="font-semibold uppercase tracking-[0.1em]"> | |||||
| Created by | |||||
| </span> | |||||
| <span | |||||
| class="truncate font-medium text-slate-800" | |||||
| :title="selected.proposal.creator" | |||||
| > | |||||
| {{ truncateWallet(selected.proposal.creator) }} | |||||
| </span> | |||||
| </div> | |||||
| </header> | |||||
| <section | |||||
| class="max-w-3xl py-8" | |||||
| aria-labelledby="proposal-content-heading" | |||||
| > | |||||
| <h2 id="proposal-content-heading" class="sr-only"> | |||||
| Proposal details | |||||
| </h2> | |||||
| <div class="content-body space-y-5 text-[0.98rem] leading-7 text-slate-800"> | |||||
| <p | |||||
| v-for="paragraph in selected.proposal.paragraphs" | |||||
| :key="paragraph.id ?? paragraph.paragraphText" | |||||
| v-html="applyFormatting(paragraph.paragraphText)" | |||||
| /> | |||||
| </div> | |||||
| </section> | |||||
| <section | |||||
| class="border-t border-slate-300 py-8" | |||||
| aria-labelledby="amendments-heading" | |||||
| > | |||||
| <div class="mb-4 flex items-baseline justify-between gap-4"> | |||||
| <h2 | |||||
| id="amendments-heading" | |||||
| class="font-header text-2xl font-bold tracking-tight text-slate-950" | |||||
| > | |||||
| Amendments | |||||
| </h2> | |||||
| <span | |||||
| class="text-[0.65rem] font-bold uppercase tracking-[0.1em] text-slate-500" | |||||
| > | |||||
| Proposed changes | |||||
| </span> | |||||
| </div> | |||||
| <AmendmentCarousel :proposal-id="currentId" /> | |||||
| </section> | |||||
| <section | |||||
| class="border-t border-slate-300 py-8" | |||||
| aria-labelledby="comments-heading" | |||||
| > | |||||
| <div class="mb-4 flex items-baseline justify-between gap-4"> | |||||
| <h2 | |||||
| id="comments-heading" | |||||
| class="font-header text-2xl font-bold tracking-tight text-slate-950" | |||||
| > | |||||
| Comments | |||||
| </h2> | |||||
| <span | |||||
| class="text-[0.65rem] font-bold uppercase tracking-[0.1em] text-slate-500" | |||||
| > | |||||
| Discussion | |||||
| </span> | |||||
| </div> | |||||
| <div | |||||
| v-if="false" | |||||
| class="rounded-lg border border-dashed border-slate-300 bg-white/50 px-5 py-8 | |||||
| text-sm text-slate-500" | |||||
| > | |||||
| Discussion will appear here. | |||||
| </div> | |||||
| <Thread | |||||
| v-for="comment in comments" | |||||
| :key="comment.id" | |||||
| :starter-comment="comment" | |||||
| class="comment-thread" | |||||
| /> | |||||
| </section> | |||||
| </div> | </div> | ||||
| </header> | |||||
| <section class="mb-6"> | |||||
| <h2 class="font-header font-bold ml-2 text-2xl tracking-tight mb-3">Amendments</h2> | |||||
| <AmendmentCarousel :issue-id="currentId"/> | |||||
| </section> | |||||
| <section class="mb-6"> | |||||
| <h2 class="font-header font-bold ml-2 text-2xl tracking-tight mb-3">Comments</h2> | |||||
| <Thread | |||||
| v-for="comment in comments" | |||||
| :key="comment.id" | |||||
| :starter-comment="comment" | |||||
| class="comment-thread" | |||||
| /> | |||||
| </section> | |||||
| <!-- Voting action rail --> | |||||
| <aside | |||||
| class="xl:sticky xl:top-6" | |||||
| aria-labelledby="vote-heading" | |||||
| > | |||||
| <section | |||||
| class="overflow-hidden rounded-xl border border-slate-300 bg-white shadow-sm" | |||||
| > | |||||
| <div class="border-b border-slate-200 px-5 py-4"> | |||||
| <h2 | |||||
| id="vote-heading" | |||||
| class="font-header text-lg font-bold tracking-tight text-slate-950" | |||||
| > | |||||
| Cast your vote | |||||
| </h2> | |||||
| <p class="mt-1 text-xs leading-5 text-slate-500"> | |||||
| Choose one position for this proposal. | |||||
| </p> | |||||
| </div> | |||||
| <div class="space-y-2 p-3"> | |||||
| <button | |||||
| type="button" | |||||
| class="group flex w-full items-center gap-3 rounded-lg border border-slate-300 | |||||
| bg-white px-3 py-2.5 text-left text-slate-700 transition | |||||
| hover:border-green-600 hover:bg-green-600 hover:text-white | |||||
| focus-visible:outline focus-visible:outline-2 | |||||
| focus-visible:outline-offset-2 focus-visible:outline-slate-950" | |||||
| > | |||||
| <Confirm | |||||
| class="size-5 shrink-0 fill-current" | |||||
| :box="96" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| <span class="font-semibold">Approve</span> | |||||
| </button> | |||||
| <button | |||||
| type="button" | |||||
| class="group flex w-full items-center gap-3 rounded-lg border border-slate-300 | |||||
| bg-white px-3 py-2.5 text-left text-slate-700 transition | |||||
| hover:border-red-600 hover:bg-red-600 hover:text-white | |||||
| focus-visible:outline focus-visible:outline-2 | |||||
| focus-visible:outline-offset-2 focus-visible:outline-slate-950" | |||||
| > | |||||
| <Cancel | |||||
| class="size-5 shrink-0 fill-current" | |||||
| :box="96" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| <span class="font-semibold">Reject</span> | |||||
| </button> | |||||
| <button | |||||
| type="button" | |||||
| class="group flex w-full items-center gap-3 rounded-lg border border-slate-300 | |||||
| bg-white px-3 py-2.5 text-left text-slate-700 transition | |||||
| hover:border-amber-500 hover:bg-amber-500 hover:text-slate-950 | |||||
| focus-visible:outline focus-visible:outline-2 | |||||
| focus-visible:outline-offset-2 focus-visible:outline-slate-950" | |||||
| > | |||||
| <Abstain | |||||
| class="size-5 shrink-0 fill-current" | |||||
| :box="96" | |||||
| aria-hidden="true" | |||||
| /> | |||||
| <span class="font-semibold">Abstain</span> | |||||
| </button> | |||||
| </div> | |||||
| <div class="border-t border-slate-200 bg-slate-50 px-5 py-4"> | |||||
| <p class="text-xs leading-5 text-slate-500"> | |||||
| Your selection can be shown here later, alongside quorum, | |||||
| voting power, and the deadline. | |||||
| </p> | |||||
| </div> | |||||
| </section> | |||||
| </aside> | |||||
| </div> | |||||
| </article> | </article> | ||||
| </template> | </template> | ||||
| <div v-if="!selected?.cid && !isLoading('content')" class="flex flex-row h-full justify-center items-center text-gray-500"> | |||||
| <span>Nothing is selected</span> | |||||
| <div | |||||
| v-else-if="!isLoading('content')" | |||||
| class="flex h-full items-center justify-center p-8" | |||||
| > | |||||
| <div class="max-w-sm text-center"> | |||||
| <p class="font-header text-2xl font-bold text-slate-700"> | |||||
| Select a proposal | |||||
| </p> | |||||
| <p class="mt-2 text-sm leading-6 text-slate-500"> | |||||
| Choose a proposal from the list to read its details, review amendments, | |||||
| and cast a vote. | |||||
| </p> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <LoadingSpinner v-if="isLoading('content')" identifier="content"/> | |||||
| <LoadingSpinner | |||||
| v-if="isLoading('content')" | |||||
| identifier="content" | |||||
| /> | |||||
| </main> | </main> | ||||
| </div> | </div> | ||||
| <!-- Toggle button for small screens --> | <!-- Toggle button for small screens --> | ||||
| <button | <button | ||||
| class="md:hidden fixed z-40 bottom-3 right-4 bg-orange-600 px-3 py-3 rounded-full shadow-lg" | |||||
| class="md:hidden fixed w-full z-40 bottom-0 left-0 bg-gray-500 px-3 py-3 text-white font-semibold" | |||||
| @click="isSidebarOpen = !isSidebarOpen" | @click="isSidebarOpen = !isSidebarOpen" | ||||
| aria-label="Toggle sidebar" | aria-label="Toggle sidebar" | ||||
| > | > | ||||
| <Menu /> | |||||
| <Stack :viewBox="36" style="display: inline-block" /> | |||||
| View Proposals | |||||
| </button> | </button> | ||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -97,8 +267,9 @@ | |||||
| import DOMPurify from "dompurify"; | import DOMPurify from "dompurify"; | ||||
| import {parse} from "marked"; | import {parse} from "marked"; | ||||
| import {computed, onMounted, onUnmounted, ref} from "vue"; | import {computed, onMounted, onUnmounted, ref} from "vue"; | ||||
| import { useWindowSize } from '@vueuse/core' | |||||
| import {ProposalList, ProposalService} from "../generated/typescript"; | import {ProposalList, ProposalService} from "../generated/typescript"; | ||||
| import {IssueSelection} from "../types/issue.ts"; | |||||
| import {ProposalSelection} from "../types/proposal.ts"; | |||||
| import {useLoading} from "../composables/useLoading.ts"; | import {useLoading} from "../composables/useLoading.ts"; | ||||
| import LoadingSpinner from "../components/LoadingSpinner.vue"; | import LoadingSpinner from "../components/LoadingSpinner.vue"; | ||||
| import {eventBus} from "../utils/eventBus.ts"; | import {eventBus} from "../utils/eventBus.ts"; | ||||
| @@ -106,34 +277,37 @@ import {CommentWithChildren} from "../types/comment.ts"; | |||||
| import Thread from "../components/Thread.vue"; | import Thread from "../components/Thread.vue"; | ||||
| import Sidebar from "../components/Sidebar.vue"; | import Sidebar from "../components/Sidebar.vue"; | ||||
| import AmendmentCarousel from "../components/AmendmentCarousel.vue"; | import AmendmentCarousel from "../components/AmendmentCarousel.vue"; | ||||
| import Menu from "../components/icons/Menu.vue"; | |||||
| import Confirm from "../components/icons/Confirm.vue"; | import Confirm from "../components/icons/Confirm.vue"; | ||||
| import Cancel from "../components/icons/Cancel.vue"; | import Cancel from "../components/icons/Cancel.vue"; | ||||
| import Abstain from "../components/icons/Abstain.vue"; | import Abstain from "../components/icons/Abstain.vue"; | ||||
| import {truncateWallet} from "../utils/wallet.ts"; | |||||
| import Stack from "../components/icons/Stack.vue"; | |||||
| import Identification from "../components/icons/Identification.vue"; | |||||
| const { register, unregister, isLoading, show: showLoading, hide: hideLoading } = useLoading(); | const { register, unregister, isLoading, show: showLoading, hide: hideLoading } = useLoading(); | ||||
| const { width } = useWindowSize(); | |||||
| register('issues', true); | |||||
| register('proposals', true); | |||||
| register('content'); | register('content'); | ||||
| const issues = ref<ProposalList>(); | |||||
| const selected = ref<IssueSelection>(); | |||||
| const proposals = ref<ProposalList>(); | |||||
| const selected = ref<ProposalSelection>(); | |||||
| const comments = ref<CommentWithChildren[]>(); | const comments = ref<CommentWithChildren[]>(); | ||||
| const isSidebarOpen = ref(false); | const isSidebarOpen = ref(false); | ||||
| const [offset, limit] = [ref(0), ref(10)]; | const [offset, limit] = [ref(0), ref(10)]; | ||||
| showLoading('issues'); | |||||
| showLoading('proposals'); | |||||
| onMounted(async () => { | onMounted(async () => { | ||||
| const issuesResponse = await ProposalService.listProposals(offset.value, limit.value); | |||||
| issues.value = issuesResponse.proposals; | |||||
| hideLoading('issues'); | |||||
| const proposalsResponse = await ProposalService.listProposals(offset.value, limit.value); | |||||
| proposals.value = proposalsResponse.proposals; | |||||
| hideLoading('proposals'); | |||||
| }); | }); | ||||
| onUnmounted(() => { | onUnmounted(() => { | ||||
| unregister('issues'); | |||||
| unregister('proposals'); | |||||
| unregister('content'); | unregister('content'); | ||||
| eventBus.off('filtersApplied'); | eventBus.off('filtersApplied'); | ||||
| @@ -144,8 +318,8 @@ const applyFormatting = (markdown: string) => { | |||||
| return DOMPurify.sanitize(parsed); | return DOMPurify.sanitize(parsed); | ||||
| } | } | ||||
| const handleSelection = async (issue: IssueSelection) => { | |||||
| selected.value = issue; | |||||
| const handleSelection = async (proposal: ProposalSelection) => { | |||||
| selected.value = proposal; | |||||
| // Close sidebar on small screens after selection | // Close sidebar on small screens after selection | ||||
| isSidebarOpen.value = false; | isSidebarOpen.value = false; | ||||
| } | } | ||||
| @@ -1 +1 @@ | |||||
| {"root":["./src/main.ts","./src/vite-env.d.ts","./src/composables/usefetch.ts","./src/composables/usefilternotifier.ts","./src/composables/useloading.ts","./src/composables/usemodal.ts","./src/composables/usesession.ts","./src/router/index.ts","./src/stores/filterstore.ts","./src/types/amendment.ts","./src/types/color.ts","./src/types/comment.ts","./src/types/dropdown.ts","./src/types/headerletter.ts","./src/types/issue.ts","./src/types/user.ts","./src/types/vote.ts","./src/utils/amendment.ts","./src/utils/eventbus.ts","./src/utils/wallet.ts","./src/app.vue","./src/components/amendmentcarousel.vue","./src/components/brandbar.vue","./src/components/chip.vue","./src/components/coloredheader.vue","./src/components/dropdown.vue","./src/components/issuecontainer.vue","./src/components/loadingspinner.vue","./src/components/postbuilder.vue","./src/components/sidebar.vue","./src/components/thread.vue","./src/components/votingcontrols.vue","./src/components/icons/abstain.vue","./src/components/icons/arrow.vue","./src/components/icons/cancel.vue","./src/components/icons/confirm.vue","./src/components/icons/downarrow.vue","./src/components/icons/filter.vue","./src/components/icons/menu.vue","./src/components/icons/speechbubbles.vue","./src/components/icons/thumbsdown.vue","./src/components/icons/thumbsup.vue","./src/components/modals/amendmentmodal.vue","./src/components/modals/filtermodal.vue","./src/components/modals/issuemodal.vue","./src/components/modals/modal.vue","./src/components/modals/registrationmodal.vue","./src/views/homeview.vue"],"errors":true,"version":"5.9.2"} | |||||
| {"root":["./src/main.ts","./src/vite-env.d.ts","./src/composables/usefetch.ts","./src/composables/usefilternotifier.ts","./src/composables/useloading.ts","./src/composables/usemodal.ts","./src/composables/usesession.ts","./src/router/index.ts","./src/stores/filterstore.ts","./src/types/amendment.ts","./src/types/color.ts","./src/types/comment.ts","./src/types/dropdown.ts","./src/types/headerletter.ts","./src/types/proposal.ts","./src/types/user.ts","./src/types/vote.ts","./src/utils/amendment.ts","./src/utils/eventbus.ts","./src/utils/wallet.ts","./src/app.vue","./src/components/amendmentcarousel.vue","./src/components/brandbar.vue","./src/components/chip.vue","./src/components/coloredheader.vue","./src/components/dropdown.vue","./src/components/issuecontainer.vue","./src/components/loadingspinner.vue","./src/components/postbuilder.vue","./src/components/sidebar.vue","./src/components/thread.vue","./src/components/votingcontrols.vue","./src/components/icons/abstain.vue","./src/components/icons/arrow.vue","./src/components/icons/cancel.vue","./src/components/icons/confirm.vue","./src/components/icons/downarrow.vue","./src/components/icons/filter.vue","./src/components/icons/menu.vue","./src/components/icons/speechbubbles.vue","./src/components/icons/thumbsdown.vue","./src/components/icons/thumbsup.vue","./src/components/modals/amendmentmodal.vue","./src/components/modals/filtermodal.vue","./src/components/modals/issuemodal.vue","./src/components/modals/modal.vue","./src/components/modals/registrationmodal.vue","./src/views/homeview.vue"],"errors":true,"version":"5.9.2"} | |||||
| @@ -11,9 +11,11 @@ export default defineConfig({ | |||||
| tailwindcss() | tailwindcss() | ||||
| ], | ], | ||||
| server: { | server: { | ||||
| port: 5173, | |||||
| proxy: { | proxy: { | ||||
| '/api': { | |||||
| target: 'http://127.0.0.1:7300', | |||||
| '/api/': { | |||||
| target: 'http://localhost:7300', | |||||
| changeOrigin: true, | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||