Skip to content

Commit

Permalink
Merge pull request #9 from nnivxix/feat/import-export-collection
Browse files Browse the repository at this point in the history
Feat/import export collection
  • Loading branch information
nnivxix authored Mar 12, 2024
2 parents fced4b6 + 655863e commit 382e1b1
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 11 deletions.
Binary file added public/icons/pwa-import-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/pwa-import.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/components/ExampleJson.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup>
defineProps({
json: Object,
});
</script>
<template>
<pre
class="border border-gray-500 border-dashed p-3 rounded-md whitespace-pre-wrap"
v-if="json"
>{{ json }}
</pre
>
<pre class="border border-gray-500 border-dashed p-3 rounded-md" v-else>
{
"id": "",
"name": "",
"description": "",
"todos": []
}
</pre
>
</template>
4 changes: 2 additions & 2 deletions src/components/FormTodo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defineEmits(["submitTodo"]);
id="name"
type="text"
v-bind="field"
class="border w-full border-spacing-1 rounded-md p-1"
class="border w-full border-spacing-1 rounded-md p-1 border-gray-400"
/>
<ErrorMessage name="name" class="text-red-600" />
</Field>
Expand All @@ -28,7 +28,7 @@ defineEmits(["submitTodo"]);
<select
v-bind="field"
:value="field.value"
class="w-full p-3 rounded-lg"
class="w-full p-3 rounded-lg border border-gray-400"
id="priority"
>
<option disabled value="">Please select one priority</option>
Expand Down
8 changes: 8 additions & 0 deletions src/pages/collection/create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,13 @@ onMounted(() => {
>
<FormCollection :form="form" />
</Form>
<div>
<p>
Already have a Tomodo json file?
<RouterLink to="/import" class="underline text-blue-700"
>now you can import it</RouterLink
>
</p>
</div>
</div>
</template>
22 changes: 14 additions & 8 deletions src/pages/collection/id.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { useRoute, useRouter } from "vue-router";
import { ref, toRaw, onMounted } from "vue";
import { Form, useForm } from "vee-validate";
import { useForm } from "vee-validate";
import * as yup from "yup";
import useCollection from "@/composables/useCollection";
Expand All @@ -11,14 +11,14 @@ import TodoItem from "@/components/TodoItem.vue";
import FormTodo from "@/components/FormTodo.vue";
import dbCollections from "@/repositories/db-collection";
import ProgressBar from "@/components/ProgressBar.vue";
import exportCollection from "@/utils/export-collection";
const route = useRoute();
const router = useRouter();
const { deleteColllection, collection, descriptionCollection } =
useCollection();
const { addTodo, markTodo, editTodo, deleteTodo, doneTodos, todos } = useTodo();
const { formTodo, isEditing, resetForm: resetFormTodo } = useFormTodo();
const vFormTodo = useForm({
validationSchema: {
name: yup.string().required().min(3).label("todo name"),
Expand Down Expand Up @@ -91,10 +91,8 @@ const handleMarkTodo = (index) => {
markTodo(index);
vFormTodo.setErrors({});
};
const handleDeleteCollection = (id) => {
/**TODO
* Change to vue modal
*/
const question = confirm("Are you sure delete this collection?");
if (question) {
deleteColllection(id);
Expand Down Expand Up @@ -136,19 +134,27 @@ onMounted(async () => {
You have {{ todos?.length }} / {{ doneTodos?.length }}
{{ todos.length > 1 ? "todos" : "todo" }}
</p>
<div class="flex gap-3">
<div class="flex gap-3 flex-wrap my-5">
<button
class="bg-red-600 text-white p-3 rounded-md my-2"
class="bg-red-600 text-white p-3 rounded-md"
@click="handleDeleteCollection(collection.id)"
>
Delete Collection
</button>
<button
@click="router.push(`/collection/${collection.id}/edit`)"
class="bg-gray-600 text-white p-3 rounded-md my-2"
class="bg-gray-600 text-white p-3 rounded-md"
type="button"
>
Edit Collection
</button>
<button
id="export-collection"
class="bg-orange-600 text-white p-3 rounded-md"
@click="exportCollection(collection)"
>
Export Collection
</button>
</div>

<div
Expand Down
143 changes: 143 additions & 0 deletions src/pages/import.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script setup>
import { Field, useForm, ErrorMessage } from "vee-validate";
import { ref, toRaw, watch } from "vue";
import { useRouter } from "vue-router";
import ExampleJson from "@/components/ExampleJson.vue";
import useCollection from "@/composables/useCollection";
import jsonParser from "@/utils/json-parser";
import validateCollection from "@/utils/validate-collection";
const formImport = useForm();
const router = useRouter();
const { addCollection, getDetailCollection, deleteColllection } =
useCollection();
const isAttached = ref(false);
const collection = ref(null);
const isCollectionExist = ref(false);
function handleDrop(event) {
event.preventDefault();
const droppedFiles = event.dataTransfer?.files;
if (droppedFiles[0].type.includes("json")) {
const file = droppedFiles[0];
isAttached.value = true;
formImport.setValues({
file,
});
formImport.setErrors({});
return;
}
formImport.setErrors({
file: "File not json",
});
}
async function onSubmit() {
try {
const data = await jsonParser(toRaw(formImport.values.file));
const collection = getDetailCollection(data.id);
if (!!collection) {
deleteColllection(data.id);
}
addCollection(data);
router.push("/");
} catch (error) {
console.error("Error parsing JSON file:", error);
formImport.setErrors({
file: "Error parsing JSON file",
});
}
}
function onClear() {
collection.value = null;
isCollectionExist.value = false;
formImport.resetForm();
}
watch(formImport.values, async (form) => {
if (!!form.file) {
const data = await jsonParser(toRaw(form.file));
if (validateCollection(data)) {
collection.value = data;
const findCollection = getDetailCollection(data.id);
if (data.id === findCollection?.id) isCollectionExist.value = true;
return;
}
formImport.setErrors({
file: "Properties on json file is not valid",
});
formImport.setFieldValue("file", null);
return;
}
});
</script>
<template>
<div>
<h1 class="text-2xl">Import Collection</h1>
<form @submit.prevent="onSubmit" class="grid gap-3">
<Field name="file" v-slot="{ handleChange }">
<div
v-if="!!collection"
class="hover:bg-[#032836] transform transition-all ease-in col-span-1 hover:text-white p-4 h-36 relative shadow-md rounded-md"
>
<h1 class="absolute bottom-3 left-5 text-xl">
{{ collection.name }} - ({{ collection.todos?.length }})
</h1>
</div>
<label for="file" v-else>
<div
@drop.prevent="handleDrop"
@dragover.prevent
class="border border-gray-600 border-dashed w-full py-9 px-5 mt-3 rounded-md"
>
<input
id="file"
accept="application/json"
type="file"
class="sr-only"
@change="handleChange"
/>
<span class="text-center block"> Upload json file </span>
</div>
</label>
<ErrorMessage name="file" class="text-red-500" />
</Field>
<div>
<button
type="submit"
class="p-3 mr-2 mt-2 bg-[#032836] text-center text-white rounded-lg"
>
Import Collection
</button>
<button
v-if="!!collection"
type="button"
@click="onClear"
class="p-3 mt-2 bg-red-500 text-center text-white rounded-lg"
>
Clear
</button>
</div>
</form>
<div class="mt-4">
<div v-if="!!!collection" class="py-3">
<p>
If the <strong>id</strong> of file is same with existing collection,
the collection will be overwrited.
</p>
<p>The json file must be structured like this.</p>
</div>
<h1 v-if="isCollectionExist" class="py-3">
The imported collection is already exist.
</h1>
<ExampleJson v-bind:json="collection" />
</div>
</div>
</template>
6 changes: 6 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Index from "../pages/index.vue";
import Create from "../pages/collection/create.vue";
import Edit from "../pages/collection/edit.vue";
import DetailCollection from "../pages/collection/id.vue";
import ImportCollection from "@/pages/import.vue";

const routes = [
{ path: "/", component: Index },
Expand All @@ -13,6 +14,11 @@ const routes = [
component: Edit,
name: "edit-collection",
},
{
path: "/import",
component: ImportCollection,
name: "import-collection",
},
];

const router = createRouter({
Expand Down
19 changes: 19 additions & 0 deletions src/utils/export-collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const exportCollection = (collection) => {
const json = JSON.stringify(collection);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = `collection-${collection.id}`;

document.body.appendChild(a);
a.click();
document.body.removeChild(a);

URL.revokeObjectURL(url);

return;
};

export default exportCollection;
16 changes: 16 additions & 0 deletions src/utils/json-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function jsonParser(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
try {
const data = JSON.parse(reader.result);
resolve(data);
} catch (error) {
reject(error);
}
};
});
}

export default jsonParser;
14 changes: 14 additions & 0 deletions src/utils/validate-collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const validateCollection = (collection) => {
if (
collection.hasOwnProperty("id") &&
collection.hasOwnProperty("name") &&
collection.hasOwnProperty("description") &&
collection.hasOwnProperty("todos") &&
Array.isArray(collection.todos)
) {
return true;
}
return false;
};

export default validateCollection;
16 changes: 15 additions & 1 deletion vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default defineConfig({
VitePWA({
registerType: "autoUpdate",
devOptions: {
enabled: false,
enabled: true,
},
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,vue}"],
Expand Down Expand Up @@ -76,6 +76,20 @@ export default defineConfig({
start_url: "/",
description: "Kelarin semuanya",
theme_color: "#ffffff",
shortcuts: [
{
name: "Import Collection",
short_name: "Import",
description: "Import the collection",
url: "/import",
icons: [
{
src: "icons/pwa-import-white.png",
sizes: "192x192",
},
],
},
],
icons: [
{
src: "icons/android-chrome-192x192.png",
Expand Down

0 comments on commit 382e1b1

Please sign in to comment.