Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { watch, computed } from 'vue';
import { nextTick } from 'process';
import { useProfilesStore } from '@/store/profiles';
import { useDraftStore } from './store/drafts';
import ChirpEditor from './components/popups/ChirpEditor.vue';

const router = useRouter();
const store = useStore();
Expand Down Expand Up @@ -145,6 +146,7 @@ watch(router.currentRoute, () => {
</div>
</main>
<UnauthPopup v-if="rootStore.$state.showUnauthPopup" />
<ChirpEditor v-if="draftStore.$state.composeChirp" />
<div id="popup"></div>
</template>

Expand Down
12 changes: 11 additions & 1 deletion src/components/TopHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ function goWrite() {
router.push(`/write`);
}

function handleChirp() {
draftStore.toggleChirp();
}

function toggleDropdown() {
showDropdown.value = !showDropdown.value;
}
Expand Down Expand Up @@ -136,7 +140,13 @@ function logout() {
>Bookmarks</router-link
>
<button
to="/write"
style="padding: 0.6rem 1.7rem"
class="mx-4 border border-primary text-primary focus:outline-none transform rounded-lg font-bold transition duration-500 ease-in-out hover:shadow-lg"
@click="handleChirp"
>
Write a Chirp
</button>
<button
style="padding: 0.6rem 1.7rem"
class="mx-4 bg-primary dark:bg-secondary text-lightButtonText focus:outline-none transform rounded-lg font-bold transition duration-500 ease-in-out hover:shadow-lg"
@click="goWrite"
Expand Down
195 changes: 195 additions & 0 deletions src/components/popups/ChirpEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<script setup lang="ts">
import CloseIcon from '@/components/icons/CloseIcon.vue';
import BrandedButton from '../BrandedButton.vue';
import PlusIcon from '../icons/PlusIcon.vue';
import XIcon from '../icons/XIcon.vue';
import { useDraftStore } from '@/store/drafts';
import { qualityTags } from '@/plugins/quality';
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { isError } from '@/plugins/helpers';
import { handleError, toastError } from '@/plugins/toast';
import { preUploadPhoto, uploadPhoto } from '@/backend/photos';
import { useStore } from '@/store/session';
import { Tag } from '@/backend/post';

const draftStore = useDraftStore();
const chirpInput = ref<HTMLInputElement>();
const tagInput = ref<HTMLInputElement>();
const featuredPhotoInput = ref<HTMLInputElement>();
const waitingImage = ref(false);
const tags = ref<string[]>([]);
const featuredPhoto = ref<string | null>(null);

function handleTag() {
const tag = tagInput.value?.value;
if (!tag) {
return;
}
const quality: { error: string } | { success: boolean } = qualityTags(tag, tags.value);
if (isError(quality)) {
toastError(quality.error);
return;
}
draftStore.addChirpTag(tag);
if (!tagInput.value) {
return;
}
tagInput.value.value = ``;
}

function removeTag(t: Tag) {
draftStore.removeChirpTag(t);
}

function handleUploadImageClick() {
if (featuredPhotoInput.value) {
const element = featuredPhotoInput.value;
element.click();
}
}

async function handleImage(e: Event) {
const eventTarget = e.target;
if (!eventTarget) {
return;
}
const target = eventTarget as HTMLInputElement;
if (!target.files || target.files.length < 1) {
return;
}
const imageFile = target.files[0];
if (!imageFile) {
return;
}
waitingImage.value = true;
try {
const { cid, image, imageName, url } = await uploadPhoto(imageFile);
await preUploadPhoto(cid, image, imageName, useStore().$state.id);
featuredPhoto.value = url as string;
draftStore.updateChirpImage(featuredPhoto.value);
} catch (err) {
handleError(err);
} finally {
target.value = ``;
waitingImage.value = false;
}
}

function removeImage() {
featuredPhoto.value = null;
draftStore.updateChirpImage(null);
}

function sendChirp() {
console.log(chirpInput.value?.value);
console.log(chirpInput.value?.value.length);
}

onMounted(() => {
if (chirpInput.value) {
chirpInput.value.value = draftStore.getChirp.content;
}
featuredPhoto.value = draftStore.getChirp.featuredPhotoCID ? draftStore.getChirp.featuredPhotoCID : null;
nextTick(() => {
chirpInput.value?.focus();
});
});

onBeforeUnmount(() => {
if (!chirpInput.value) {
return;
}
const c = chirpInput.value.value;
draftStore.updateChirp(c);
});
</script>

<template>
<div
class="popup bg-darkBG dark:bg-gray5 modal-animation fixed top-0 bottom-0 left-0 right-0 z-40 flex h-screen w-full items-start justify-center bg-opacity-50 dark:bg-opacity-50"
@click.self="draftStore.toggleChirp"
>
<div
class="popup popupCard w-full lg:w-600 min-h-40 max-h-90 bg-lightBG dark:bg-darkBGStop card-animation mt-12 overflow-y-auto rounded-lg p-6 pt-4 shadow-lg"
>
<!-- Header and close icon -->
<div class="flex items-center justify-between pb-6">
<h1 class="text-lightPrimaryText dark:text-darkPrimaryText text-4xl font-semibold">Write a Chirp</h1>
<button class="bg-gray1 dark:bg-gray5 focus:outline-none rounded-full p-1" @click="draftStore.toggleChirp">
<CloseIcon />
</button>
</div>
<!-- Input textarea -->
<div class="">
<textarea
ref="chirpInput"
placeholder="What's your Chirp?"
class="w-full h-32 resize-none bg-gray1 dark:bg-gray5 rounded-lg focus:outline-none px-2 py-1 border focus:border-primary"
/>
</div>
<!-- Featured photo -->
<div class="my-4">
<div
v-if="waitingImage"
class="w-full h-full bg-lightInput dark:bg-gray7 rounded-lg animate-pulse flex justify-center items-center"
>
<p class="text-sm text-gray5 dark:text-gray3">Uploading image...</p>
</div>
<div v-if="featuredPhoto !== null && !waitingImage" class="h-full w-full">
<img :src="featuredPhoto" class="h-40 w-full object-cover rounded-lg" />
</div>
</div>
<!-- Post metadata -->
<div class="text-sm text-lightSecondaryText flex justify-between mb-4 mt-4">
<!-- Add tags -->
<div class="flex flex-row-reverse">
<input
v-show="tags.length < 3"
ref="tagInput"
class="focus:outline-none bg-gray1 dark:bg-gray5 rounded-lg px-2 py-1 border border-gray1 focus:border-primary"
type="text"
placeholder="Tag"
@keypress.enter="handleTag"
/>
<button
v-for="t in draftStore.chirp.tags"
:key="t.name"
class="bg-gray1 px-2 py-1 mr-2 rounded-lg flex items-center"
@click="removeTag(t)"
>
{{ t.name }}
<XIcon class="p-1" />
</button>
</div>
<!-- Add image -->
<button
v-if="featuredPhoto === null"
:disabled="waitingImage"
class="text-primary flex items-center"
@click="handleUploadImageClick"
>
<PlusIcon class="p-1" />Add image
</button>
<!-- Photo Uploaded -->
<div v-else>
<button class="text-primary focus:outline-none text-sm" @click="handleUploadImageClick">Change Image</button>
<button class="text-negative focus:outline-none ml-4 text-sm" @click="removeImage">Remove Image</button>
</div>
<input
id="featured-photo"
ref="featuredPhotoInput"
class="hidden"
name="photo"
type="file"
accept="image/jpeg, image/png"
@change="handleImage"
/>
</div>
<!-- Post and convert buttons -->
<div class="flex justify-between items-center">
<button class="text-gray5 text-semibold">Convert to Post</button>
<BrandedButton :text="`Send Chirp`" :action="sendChirp" />
</div>
</div>
</div>
</template>
15 changes: 11 additions & 4 deletions src/components/post/SimpleFeedCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,24 @@ onBeforeMount(() => {
<div class="mt-4 flex flex-col justify-between xl:flex-row">
<!-- Left side: Title, subtitle / preview, tags -->
<div class="mr-4 flex w-full flex-col justify-between">
<!-- Regular post -->
<div class="cursor-pointer" @click="handlePostRedirect">
<div class="flex max-w-full flex-col overflow-hidden pr-4">
<!-- Short post -->
<!-- TODO: Check if short post -->
<div v-if="true" class="flex max-w-full flex-col overflow-hidden pr-4">
<div class="break-words pb-2 text-lg font-semibold dark:text-darkPrimaryText">tweet style goes here</div>
</div>
<!-- Long post -->
<div v-else class="flex max-w-full flex-col overflow-hidden pr-4">
<div class="flex flex-row w-full justify-between">
<h3 class="break-words pb-2 text-lg font-semibold dark:text-darkPrimaryText">
{{ fetchedPost.post.title
}}<CrownIcon v-if="fetchedPost.post.encrypted" class="ml-2 inline text-neutral w-5 h-5 -mt-1" />
{{ fetchedPost?.post.title
}}<CrownIcon v-if="fetchedPost?.post.encrypted" class="ml-2 inline text-neutral w-5 h-5 -mt-1" />
</h3>
</div>
<h6 class="break-words text-lightSecondaryText dark:text-darkSecondaryText">
{{
fetchedPost.post.subtitle ? fetchedPost.post.subtitle : createPostExcerpt(fetchedPost.post.excerpt)
fetchedPost?.post.subtitle ? fetchedPost?.post.subtitle : createPostExcerpt(fetchedPost?.post.excerpt)
}}
</h6>
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/post.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export function createPostExcerpt(e: string): string {
export function createPostExcerpt(e: string | undefined): string {
if (!e) {
return ``;
}
const excerpt = e.slice(0, 177).trim();
if (excerpt.endsWith(`...`)) {
return excerpt;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/quality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function qualityFeaturedPhotoCaption(featuredPhotoCaption: string): Check
return { success: true };
}
export function qualityTags(tag: string, tags?: Array<any>): CheckResult {
console.log(tag, tags);
if (tag.trim().length < textLimits.post_tag.min) {
return { error: `Tag length cannot be less than ${textLimits.post_tag.min} characters` };
}
Expand Down
39 changes: 39 additions & 0 deletions src/store/drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ export interface DraftPost extends Post {
editorImageKeys: EditorImages;
}

export interface IChirp {
authorID: string;
content: string;
timestamp: number;
featuredPhotoCID?: string | null;
tags: Tag[];
}

export interface DraftStore {
drafts: DraftPost[];
activeIndex: number;
draftWidget: boolean;
hasPosted: boolean;
isPosting: boolean;
composeChirp: boolean;
chirp: IChirp;
}

export const useDraftStore = defineStore(`draftStore`, {
Expand All @@ -24,6 +34,14 @@ export const useDraftStore = defineStore(`draftStore`, {
draftWidget: false,
hasPosted: false,
isPosting: false,
composeChirp: false,
chirp: {
authorID: useStore().$state.id,
content: ``,
timestamp: Date.now(),
featuredPhotoCID: null,
tags: [],
},
};
},
persist: true,
Expand All @@ -46,8 +64,29 @@ export const useDraftStore = defineStore(`draftStore`, {
getIsPosting: (state: DraftStore) => {
return state.isPosting;
},
getChirp: (state: DraftStore) => {
return state.chirp;
},
},
actions: {
toggleChirp() {
this.composeChirp = !this.composeChirp;
},
updateChirp(c: string) {
this.chirp.content = c;
},
addChirpTag(tag: string) {
this.chirp.tags.push({ name: tag });
},
updateChirpImage(cid: string | null) {
this.chirp.featuredPhotoCID = cid;
},
removeChirpTag(tag: Tag) {
const i = this.chirp.tags.indexOf(tag);
if (i > -1) {
this.chirp.tags.splice(i, 1);
}
},
setActiveDraft(index: number) {
this.activeIndex = index;
},
Expand Down