import {reactive} from "vue";
export default function useForm(fields) {
return reactive({
fields,
processing: false,
error: null,
async submit(submitter) {
if (this.processing) return;
this.error = null;
this.processing = true;
try {
await submitter(this.fields);
} catch (err) {
this.error = err;
}
this.processing = false;
},
});
}
// https://tallpad.com/series/vuejs-misc/lessons/vue-state-and-localstorage-perfect-sync-made-simple
// onMounted => SSR
import {onMounted, ref, watch} from "vue";
export default function (initialValue, key) {
const val = ref(initialValue);
onMounted(() => {
const storageVal = window.localStorage.getItem(key);
if (storageVal) {
val.value = JSON.parse(storageVal);
}
watch(
val,
val => {
window.localStorage.setItem(key, JSON.stringify(val));
},
{deep: true}
);
});
return val;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tallpad</title>
</head>
<body>
<div id="app"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_API_KEY&libraries=places"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
const img = document.querySelector('img')
const tempImg = new Image()
tempImg.src = 'https://masoprofit.cz/storage/images/1600x2000/41847.png.webp'
tempImg.decode().then(() => {
img.src = tempImg.src
}).catch(e => {
console.log(e)
})
// https://tallpad.com/series/vuejs-misc/lessons/table-of-contents-component-using-vuejs
<script setup>
import {onMounted, ref, computed} from "vue";
import TocList from "./TocList.vue";
import {slugifyWithCounter} from "@sindresorhus/slugify";
const props = defineProps({
contentSelector: {
type: String,
required: true,
},
});
const slugify = slugifyWithCounter();
const headings = ref([]);
onMounted(() => {
window.document
.querySelector(props.contentSelector)
.querySelectorAll("h1, h2, h3, h4, h5, h6")
.forEach(el => {
let id = slugify(el.innerText);
el.setAttribute("id", id);
headings.value.push({
id: id,
level: parseInt(el.tagName.charAt(1), 10),
content: el.innerText,
subheadings: [],
});
});
});
const groupedHeadings = computed(() => {
let items = [...headings.value];
for (let i = items.length - 1; i >= 0; i--) {
let currentItem = items[i];
let parentItem = items.findLast((item, index) => {
return item.level < currentItem.level && index < i;
});
if (parentItem) {
parentItem.subheadings.unshift(currentItem);
items.splice(i, 1);
}
}
return items;
});
</script>
<template>
<div class="bg-slate-50 -mx-6 p-6" v-if="groupedHeadings.length">
<h3
class="border-b-2 border-slate-300 inline-block uppercase font-bold tracking-wide text-slate-800 mb-5"
>
Contents:
</h3>
<TocList :items="groupedHeadings" />
</div>
</template>
// https://tallpad.com/series/vuejs-misc/lessons/build-an-alert-component-using-vue-tailwindcss-and-cva
<script setup>
import {InformationCircleIcon, XMarkIcon, CheckCircleIcon, ExclamationTriangleIcon, XCircleIcon} from "@heroicons/vue/20/solid/index.js";
import {computed} from "vue";
import {cva} from "class-variance-authority";
const props = defineProps({
intent: {
type: String,
validator(value) {
return ["info", "success", "danger", "warning"].includes(value);
},
default: "info",
},
title: String,
show: {
type: Boolean,
default: true,
},
onDismiss: Function,
});
const containerClass = computed(() => {
return cva("flex p-4 rounded-md space-x-3", {
variants: {
intent: {
info: "bg-blue-100",
success: "bg-green-100",
warning: "bg-orange-100",
danger: "bg-red-100",
},
},
})({
intent: props.intent,
});
});
const iconClass = computed(() => {
return cva("w-6 h-6", {
variants: {
intent: {
info: "text-blue-700",
success: "text-green-600",
warning: "text-orange-400",
danger: "text-red-500",
},
},
})({
intent: props.intent,
});
});
const titleClass = computed(() => {
return cva("font-medium", {
variants: {
intent: {
info: "text-blue-900",
success: "text-green-900",
warning: "text-orange-900",
danger: "text-red-900"
},
},
})({
intent: props.intent,
});
});
const contentClass = computed(() => {
return cva("text-sm", {
variants: {
intent: {
info: "text-blue-800",
success: "text-green-800",
warning: "text-orange-800",
danger: "text-red-800",
},
},
})({
intent: props.intent,
});
});
const closeButtonClass = computed(() => {
return cva("p-0.5 rounded-md -m-1", {
variants: {
intent: {
info: "text-blue-900/70 hover:text-blue-900 hover:bg-blue-200",
success: "text-green-900/70 hover:text-green-900 hover:bg-green-200",
warning: "text-orange-900/70 hover:text-orange-900 hover:bg-orange-200",
danger: "text-red-900/70 hover:text-red-900 hover:bg-red-200",
},
},
})({
intent: props.intent,
});
});
const iconComponent = computed(() => {
const icons = {
success: CheckCircleIcon,
warning: ExclamationTriangleIcon,
danger: XCircleIcon,
info: InformationCircleIcon,
};
return icons[props.intent];
});
function dismiss() {
if (props.onDismiss) {
props.onDismiss();
}
}
</script>
<template>
<div v-if="props.show" :class="containerClass">
<div class="shrink-0">
<component :is="iconComponent" :class="iconClass" />
</div>
<div class="flex-1 space-y-2">
<h2 :class="titleClass">
{{ props.title }}
</h2>
<div :class="contentClass"><slot /></div>
</div>
<div class="shrink-0" v-if="props.onDismiss">
<button @click="dismiss()" :class="closeButtonClass">
<XMarkIcon class="w-6 h-6" />
</button>
</div>
</div>
</template>
SET @cnt = (SELECT COUNT(1) FROM image_size WHERE folder = '840x1190');
INSERT INTO `image_size`
(`id`, `folder`, `quality`, `xsize`, `ysize`,
`image_resize_id`, `diagonal`, `watermark__image_id`,
`watermark_xsize`, `watermark_ysize`,
`watermark_xpos`, `watermark_ypos`,
`crop_left`, `crop_top`, `trim`, `grayscale`,
`filter_color`, `filter_opacity`)
SELECT NULL, '840x1190', 95, 840, 1190,
0, NULL, NULL,
NULL, NULL,
NULL, NULL,
NULL, NULL, 0, 0,
NULL, NULL
WHERE @cnt = 0;
<script setup>
import {reactive, watch} from "vue";
import BaseLabel from "./BaseLabel.vue";
import BaseInput from "./BaseInput.vue";
import BaseTextarea from "./BaseTextarea.vue";
import SecondaryButton from "./SecondaryButton.vue";
import BaseButton from "./BaseButton.vue";
import BaseCheckbox from "./BaseCheckbox.vue";
import BaseSwitch from "./BaseSwitch.vue";
const form = reactive({
name: null,
is_sellable: false,
is_available_online: false,
is_available_in_stores: false,
price: null,
description: null,
});
/**
* Separate watchers
*/
watch(
() => form.is_available_online,
isAvailableOnline => {
if (!isAvailableOnline && !form.is_available_in_stores) {
form.is_sellable = false;
}
}
);
watch(
() => form.is_available_in_stores,
isAvailableInStores => {
if (!isAvailableInStores && !form.is_available_online) {
form.is_sellable = false;
}
}
);
/**
* Watch multiple values in the same watch function
*/
watch(
() => [form.is_available_online, form.is_available_in_stores],
([isAvailableOnline, isAvailableInStores]) => {
if (!isAvailableOnline && !isAvailableInStores) {
form.is_sellable = false;
}
}
);
/**
* Combine watched values
*/
watch(
() => form.is_available_online || form.is_available_in_stores,
isSellable => (form.is_sellable = isSellable)
);
</script>
<template>
<form class="bg-white p-6 rounded-xl shadow">
<div class="space-y-6 sm:space-y-5">
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel required>Name</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseInput
v-model="form.name"
placeholder="Enter product name"
type="text"
></BaseInput>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel>Can be sold?</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseSwitch v-model="form.is_sellable"></BaseSwitch>
</div>
</div>
<template v-if="form.is_sellable">
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel>Available in online stores</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseCheckbox v-model:checked="form.is_available_online" />
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel>Available in physical stores</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseCheckbox v-model:checked="form.is_available_in_stores" />
</div>
</div>
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel>Price</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseInput
v-model="form.price"
placeholder="Enter price"
type="number"
></BaseInput>
</div>
</div>
</template>
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start">
<BaseLabel>Description</BaseLabel>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<BaseTextarea
v-model="form.description"
placeholder="Enter product description"
rows="3"
></BaseTextarea>
</div>
</div>
</div>
<div class="pt-5">
<div class="flex justify-end">
<SecondaryButton type="button">Cancel</SecondaryButton>
<BaseButton type="submit">Save</BaseButton>
</div>
</div>
</form>
</template>
<script setup>
import {ref, computed} from "vue";
const people = ref([
{id: 1, firstName: 'Constantin', lastName: 'Druc'},
{id: 2, firstName: 'Jack', lastName: 'Dorsey'},
{id: 3, firstName: 'Bill', lastName: 'Burr'},
{id: 4, firstName: 'Hugh', lastName: 'Jackman'},
{id: 5, firstName: 'Tracey', lastName: 'Johnes'},
]);
function fullName(firstName, lastName) {
console.log('fullName() was executed');
return firstName + ' ' + lastName;
}
</script>
<template>
<ul>
<li v-for="person in people" :key="person.id">
{{ fullName(person.firstName, person.lastName) }}
</li>
</ul>
</template>