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>
// 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>
<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>
<script setup>
import ShoppingCart from "./ShoppingCart.vue";
import {ref} from "vue";
const isOpened = ref(false);
</script>
<template>
<button @click="isOpened = true">Open cart</button>
<ShoppingCart :is-opened="isOpened" @toggle="(value) => isOpened = value"/>
</template>
<script setup>
import axios from "axios";
import Player from "@vimeo/player";
import {throttle} from "lodash";
import ForwardIcon from "./svgs/ForwardIcon";
import BackwardIcon from "./svgs/BackwardIcon";
import LoadingIcon from "./svgs/LoadingIcon";
import {computed, onMounted, onUnmounted, ref, useSlots} from "vue";
const props = defineProps({
lessonId: Number,
trackProgress: Boolean
});
const speedIndex = ref(1);
let player = null;
const speedOptions = [0.75, 1, 1.25, 1.5, 1.75, 2.0];
const currentSpeed = computed(() => speedOptions[speedIndex.value]);
if (window.localStorage.getItem('speedIndex')) {
speedIndex.value = parseInt(window.localStorage.getItem('speedIndex'));
}
onMounted(() => {
player = new Player(useSlots().default()[0].el);
player.on('play', () => player.setPlaybackRate(currentSpeed.value));
if (props.trackProgress) {
player.on('progress', throttle((event) => updateProgress(event.percent), 10000));
player.on('ended', () => updateProgress(1));
}
});
onUnmounted(() => {
player.off('play');
player.off('progress');
player.off('ended');
});
function updateProgress(percent) {
return axios.put(`/lessons/${props.lessonId}/progress`, {percent: percent});
}
function toggleSpeed() {
if (typeof speedOptions[speedIndex.value + 1] !== "undefined") {
speedIndex.value += 1;
} else {
speedIndex.value = 0;
}
window.localStorage.setItem('speedIndex', speedIndex.value);
player.setPlaybackRate(currentSpeed.value);
}
async function goForward(seconds) {
const currentTime = await player.getCurrentTime();
const totalDuration = await player.getDuration();
player.setCurrentTime(Math.min(totalDuration, (currentTime + seconds)));
}
async function goBack(seconds) {
const currentTime = await player.getCurrentTime();
player.setCurrentTime(Math.max(0, (currentTime - seconds)));
}
</script>
<template>
<div class="relative aspect-w-16 aspect-h-9">
<div class="flex absolute inset-0 justify-center items-center">
<LoadingIcon class="w-14 h-14 text-white animate-spin"></LoadingIcon>
</div>
<slot></slot>
</div>
<div class="flex justify-end">
<div class="grid grid-cols-5 w-full max-w-xl divide-x divide-gray-700">
<button
class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
@click="toggleSpeed()">
<span class="hidden sm:inline-block">Speed:</span><span>{{ currentSpeed }}x</span>
</button>
<button
class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
@click="goBack(5)">
<BackwardIcon class="w-4 h-4"/>
<span>5s</span>
</button>
<button
class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
@click="goForward(5)">
<span>5s</span>
<ForwardIcon class="w-4 h-4"/>
</button>
<button
class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
@click="goBack(10)">
<BackwardIcon class="w-4 h-4"/>
<span>10s</span>
</button>
<button
class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
@click="goForward(10)">
<span>10s</span>
<ForwardIcon class="w-4 h-4"/>
</button>
</div>
</div>
</template>
import {onUnmounted} from "vue";
export default function (src) {
return new Promise((resolve, reject) => {
let script = document.querySelector(`script[src="${src}"]`);
if (!script) {
script = document.createElement("script");
script.src = src;
script.async = true;
script.setAttribute("data-status", "loading");
document.head.append(script);
}
if (script.getAttribute("data-status") === "loaded") {
resolve();
}
function onScriptLoad() {
resolve();
script.setAttribute("data-status", "loaded");
}
function onScriptError() {
reject();
script.setAttribute("data-status", "error");
}
script.addEventListener("load", onScriptLoad);
script.addEventListener("error", onScriptError);
onUnmounted(() => {
if (document.head.contains(script)) {
script.removeEventListener("load", onScriptLoad);
script.removeEventListener("error", onScriptError);
document.head.removeChild(script);
}
})
});
}