Skip to content

Commit 586269d

Browse files
Merge pull request #3 from vueschool/day-1-catchup
Day 1 catchup
2 parents d7244f0 + 0c4df57 commit 586269d

17 files changed

+370
-159
lines changed

components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ declare module '@vue/runtime-core' {
88
AppButton: typeof import('./src/components/AppButton.vue')['default']
99
AppImage: typeof import('./src/components/AppImage.vue')['default']
1010
AppImageDropzone: typeof import('./src/components/AppImageDropzone.vue')['default']
11+
AppLoader: typeof import('./src/components/AppLoader.vue')['default']
1112
AppPageHeading: typeof import('./src/components/AppPageHeading.vue')['default']
1213
BoardCard: typeof import('./src/components/BoardCard.vue')['default']
1314
BoardDragAndDrop: typeof import('./src/components/BoardDragAndDrop.vue')['default']

src/components/AppImage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<template>
2-
<img v-bind="$attrs" />
2+
<img />
33
</template>
Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,63 @@
11
<script setup lang="ts">
2-
import { useBase64, useDropZone } from "@vueuse/core";
3-
import { ref } from "vue";
2+
import { useDropZone, useBase64 } from "@vueuse/core";
3+
import { ref, computed } from "vue";
4+
import use8baseStorage from "@/composables/use8baseStorage";
5+
const props = defineProps<{
6+
image?: string;
7+
loading?: boolean;
8+
}>();
9+
const emit = defineEmits<{
10+
(e: "upload", payload: { id: string }): void;
11+
}>();
12+
// data
13+
const image = ref<string | File | null | undefined>(props.image);
14+
const dropZoneRef = ref(null);
415
5-
const dropzoneEl = ref<HTMLElement | null>(null);
6-
const file = ref();
7-
const { base64: url } = useBase64(file);
8-
const { isOverDropZone } = useDropZone(dropzoneEl, (files) => {
9-
if (!files) return;
10-
file.value = files[0];
11-
console.log(file.value);
12-
});
16+
//@ts-expect-error is checked in src for string type
17+
const { base64 } = useBase64(image);
18+
const uploadingToFilestack = ref(false);
19+
const src = computed(() =>
20+
typeof image.value === "string" ? image.value : base64.value
21+
);
1322
14-
function onFileChange(e: any) {
15-
file.value = e.target.files[0];
16-
console.log(file.value);
23+
// functions
24+
function onFileSelect(e: Event) {
25+
handleFiles((e.target as HTMLInputElement).files);
1726
}
18-
19-
function reset() {
20-
file.value = null;
27+
function onDrop(files: File[] | null) {
28+
handleFiles(files);
29+
}
30+
const { uploadAsset } = use8baseStorage();
31+
async function handleFiles(files: FileList | File[] | null) {
32+
if (!files) return;
33+
image.value = files[0];
34+
uploadingToFilestack.value = true;
35+
const res = await uploadAsset(files[0]);
36+
emit("upload", res?.data.fileCreate);
37+
uploadingToFilestack.value = false;
2138
}
39+
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop);
2240
</script>
2341

2442
<template>
2543
<div
26-
ref="dropzoneEl"
27-
class="flex m-4"
44+
class="bg-gray-100 p-2 flex justify-center items-center border-2 border-gray-100 relative"
2845
:class="{
29-
border: isOverDropZone,
30-
'bg-orange-100': isOverDropZone,
31-
'border-orange-400': isOverDropZone,
46+
'border-blue-200': isOverDropZone,
47+
'border-2': isOverDropZone,
3248
}"
33-
style="width: 300px; height: 200px; background: #3332; position: relative"
49+
ref="dropZoneRef"
3450
>
35-
<div class="m-auto opacity-50">Drop a image over or select</div>
36-
<div v-if="url" class="absolute left-0 top-0 bottom-0 right-0 object-cover">
37-
<img :src="url" class="h-full" />
38-
</div>
39-
<input
40-
class="absolute z-1 left-0 top-0 bottom-0 right-0 opacity-0"
41-
type="file"
42-
accept="image/*"
43-
@input="onFileChange"
44-
/>
51+
<label class="absolute top-0 left-0 right-0 bottom-0 block">
52+
<input
53+
accept="image/png, image/jpeg"
54+
class="hidden"
55+
type="file"
56+
@change="onFileSelect"
57+
/>
58+
</label>
59+
<AppImage v-if="image" :src="src" />
60+
<template v-else>{{ "Click or drop to upload image" }}</template>
61+
<AppLoader v-if="loading || uploadingToFilestack" :overlay="true" />
4562
</div>
46-
<button @click="reset">Reset</button>
4763
</template>

src/components/AppLoader.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import { Loader as KLoader } from "@progress/kendo-vue-indicators";
3+
defineProps<{
4+
overlay: boolean;
5+
}>();
6+
</script>
7+
<template>
8+
<div :class="{ overlay }">
9+
<KLoader type="pulsing" />
10+
</div>
11+
</template>
12+
13+
<style scoped>
14+
.overlay {
15+
@apply absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center;
16+
background: rgba(255, 255, 255, 0.95);
17+
z-index: 2;
18+
}
19+
</style>

src/components/BoardDragAndDrop.vue

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const alerts = useAlerts();
1111
const props = defineProps<{
1212
board: Board;
1313
tasks: Task[];
14-
addTask(task: Partial<Task>): Task;
14+
addTask(task: Partial<Task>): Promise<Task>;
1515
}>();
1616
1717
// events
@@ -22,7 +22,11 @@ const emit = defineEmits<{
2222
// local data
2323
const tasks = reactive(cloneDeep(props.tasks));
2424
const board = reactive(cloneDeep(props.board));
25-
const columns = reactive<Column[]>(JSON.parse(board.order as string));
25+
const columns = reactive<Column[]>(
26+
typeof board.order === "string"
27+
? JSON.parse(board.order as string)
28+
: board.order
29+
);
2630
2731
// methods
2832
function addColumn() {
@@ -36,7 +40,7 @@ function addColumn() {
3640
watch(columns, () => {
3741
emit(
3842
"update",
39-
cloneDeep({ ...board, order: JSON.stringify(toRaw(columns)) })
43+
cloneDeep({ ...props.board, order: JSON.stringify(toRaw(columns)) })
4044
);
4145
});
4246
@@ -53,7 +57,7 @@ async function addTask({ column, title }: { column: Column; title: string }) {
5357
</script>
5458

5559
<template>
56-
<div class="flex py-12 items-start">
60+
<div class="flex py-12 items-start max-w-full overflow-x-auto">
5761
<draggable
5862
:list="columns"
5963
group="columns"
@@ -65,11 +69,19 @@ async function addTask({ column, title }: { column: Column; title: string }) {
6569
class="column bg-gray-100 flex flex-col justify-between rounded-lg px-3 py-3 rounded mr-4 w-[300px]"
6670
>
6771
<div>
68-
<h3>{{ column.title }}</h3>
72+
<h3>
73+
<input
74+
type="text"
75+
:value="column.title"
76+
class="bg-transparent mb-2"
77+
@keydown.enter="($event.target as HTMLInputElement).blur()"
78+
@blur="column.title = ($event.target as HTMLInputElement).value"
79+
/>
80+
</h3>
6981
<draggable
7082
:list="column.taskIds"
7183
group="tasks"
72-
item-key="uid"
84+
item-key="id"
7385
:animation="200"
7486
ghost-class="ghost-card"
7587
class="min-h-[400px]"
@@ -94,6 +106,11 @@ async function addTask({ column, title }: { column: Column; title: string }) {
94106
</div>
95107
</template>
96108
</draggable>
97-
<button class="text-gray-500" @click="addColumn">New Column +</button>
109+
<button
110+
class="text-gray-500 whitespace-nowrap pr-10 block"
111+
@click="addColumn"
112+
>
113+
New Column +
114+
</button>
98115
</div>
99116
</template>

src/components/BoardMenu.vue

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,39 @@ import { Popup as KPopup } from "@progress/kendo-vue-popup";
33
import { Button as KButton } from "@progress/kendo-vue-buttons";
44
import { ref } from "vue";
55
import type { Board } from "@/types";
6+
import { onClickOutside } from "@vueuse/core";
7+
import { useMutation } from "@vue/apollo-composable";
8+
import attachImageToBoardMutation from "@/graphql/mutations/attachImageToBoard.mutation.gql";
9+
import { useAlerts } from "@/stores/alerts";
10+
11+
const alerts = useAlerts();
12+
613
const props = defineProps<{
714
board: Board;
815
}>();
16+
917
const show = ref(false);
1018
const menu = ref(null);
11-
defineEmits<{
19+
onClickOutside(menu, () => setTimeout(() => (show.value = false), 2));
20+
21+
const emit = defineEmits<{
1222
(e: "deleteBoard", payload: null): void;
23+
(e: "imageUpload", payload: { id: string }): void;
1324
}>();
25+
26+
const {
27+
mutate: attachImageToBoard,
28+
onError: errorAttachingImage,
29+
onDone: onImageAttached,
30+
loading: imageLoading,
31+
} = useMutation(attachImageToBoardMutation);
32+
errorAttachingImage((error) => {
33+
console.log(error);
34+
alerts.error("Error setting board image");
35+
});
36+
onImageAttached((result) => {
37+
emit("imageUpload", result.data.boardUpdate.image);
38+
});
1439
</script>
1540
<template>
1641
<div>
@@ -42,6 +67,20 @@ defineEmits<{
4267
Delete Board
4368
</button>
4469
</li>
70+
<li>
71+
<strong>Board Image</strong>
72+
<AppImageDropzone
73+
class="aspect-video w-56"
74+
:image="board.image?.downloadUrl"
75+
:loading="imageLoading"
76+
@upload="
77+
attachImageToBoard({
78+
id: board.id,
79+
imageId: $event.id,
80+
})
81+
"
82+
/>
83+
</li>
4584
</ul>
4685
</div>
4786
</KPopup>

src/components/TaskCreator.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const emit = defineEmits<{
1010
}>();
1111
const handleActivate = () => {
1212
active.value = true;
13-
nextTick(() => input.value.focus());
13+
nextTick(() => input.value?.focus());
1414
};
1515
const handleEnter = () => {
1616
emit("create", value.value);

src/components/TheDrawer.vue

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,56 @@
11
<script setup lang="ts">
22
import { Drawer, DrawerContent } from "@progress/kendo-vue-layout";
33
import { useLocalStorage } from "@vueuse/core";
4-
import { useRouter } from "vue-router";
4+
import { useRouter, useRoute } from "vue-router";
55
66
import { computed, ref } from "vue";
77
88
const router = useRouter();
9-
const selectedId = ref(0);
9+
const route = useRoute();
1010
1111
const expanded = useLocalStorage("vue-forge-drawer-expanded", true);
1212
const expandedIcon = computed(() =>
1313
expanded.value ? "k-i-arrow-chevron-left" : "k-i-arrow-chevron-right"
1414
);
15-
const items = computed(() => [
15+
const items = computed(() =>
16+
[
1617
{
17-
text: "Boards",
18-
icon: "k-i-set-column-position",
19-
data: {
20-
path: "/",
18+
text: "Boards",
19+
icon: "k-i-set-column-position",
20+
data: {
21+
path: "/boards",
22+
},
2123
},
22-
},
23-
{
24-
text: "Templates",
25-
icon: "k-i-border-left",
26-
data: {
27-
path: "/templates",
24+
{
25+
text: "Templates",
26+
icon: "k-i-border-left",
27+
data: {
28+
path: "/templates",
29+
},
2830
},
29-
},
30-
{
31-
text: "Settings",
32-
icon: "k-i-gear",
33-
data: {
34-
path: "/settings",
31+
{
32+
text: "Settings",
33+
icon: "k-i-gear",
34+
data: {
35+
path: "/settings",
36+
},
3537
},
36-
},
37-
{
38-
text: "Collapse",
39-
icon: expandedIcon.value,
40-
data: {
41-
action: () => (expanded.value = !expanded.value),
38+
{
39+
text: "Collapse",
40+
icon: expandedIcon.value,
41+
data: {
42+
action: () => (expanded.value = !expanded.value),
43+
},
4244
},
43-
},
44-
].map((item, index) => ({
45-
...item,
46-
selected: index === selectedId.value,
45+
].map((item) => ({
46+
...item,
47+
selected: item.data.path ? route.path.startsWith(item.data.path) : false,
4748
}))
4849
);
4950
5051
function onSelect({ itemIndex }: { itemIndex: number }) {
5152
const item = items.value[itemIndex];
52-
selectedId.value = itemIndex;
53+
if (!item) return;
5354
if (item.data.path) router.push(item.data.path);
5455
if (typeof item.data.action === "function") item.data.action();
5556
}
@@ -65,8 +66,8 @@ function onSelect({ itemIndex }: { itemIndex: number }) {
6566
:items="items"
6667
@select="onSelect"
6768
>
68-
<DrawerContent>
69-
<div class="px-5">
69+
<DrawerContent class="max-w-full overflow-hidden">
70+
<div class="p-5">
7071
<router-view />
7172
</div>
7273
</DrawerContent>

src/graphql/apolloClient.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {
2+
ApolloClient,
3+
createHttpLink,
4+
InMemoryCache,
5+
} from "@apollo/client/core";
6+
const httpLink = createHttpLink({
7+
uri: "https://api.8base.com/cl5ittcmt05gm09kz812y1b3t",
8+
});
9+
10+
const cache = new InMemoryCache();
11+
12+
export const apolloClient = new ApolloClient({
13+
link: httpLink,
14+
cache,
15+
});

0 commit comments

Comments
 (0)