Refactor analytics events (#209)

* Refactor analytics.js

* Update analytics calls in app.vue

* Update analytics calls in presets.vue

* Update analytics calls in tools.vue (and app.vue)

* Update analytics calls in global.vue

* Update analytics calls in domain.vue

* Update analytics calls in setup.vue

* Add list of all events to analytics.js

* Add custom copy to clipboard that emits event

* Emit the events from the components

* Update copyright year in all files touched

* Update analytics calls in download.vue

* Update analytics calls in ssl.vue

* Update analytics calls in certbot.vue

* Update analytics calls in domain.vue

* Update analytics calls in app.vue

* Note down 'Code snippet copied' events
pull/214/head
Matt (IPv4) Cowley 2021-01-18 19:45:19 +00:00 committed by GitHub
parent c86fb3cf76
commit 3fdccfa68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 650 additions and 113 deletions

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -101,6 +101,7 @@ THE SOFTWARE.
:name="confContents[0]" :name="confContents[0]"
:conf="confContents[1]" :conf="confContents[1]"
:half="Object.keys(confFilesOutput).length > 1 && !splitColumn" :half="Object.keys(confFilesOutput).length > 1 && !splitColumn"
@copied="codeCopiedEvent(confContents[3])"
></component> ></component>
</template> </template>
</div> </div>
@ -125,7 +126,6 @@ THE SOFTWARE.
import isObject from '../util/is_object'; import isObject from '../util/is_object';
import analytics from '../util/analytics'; import analytics from '../util/analytics';
import browserLanguage from '../util/browser_language'; import browserLanguage from '../util/browser_language';
import { toSep } from '../util/language_pack_name';
import { defaultPack } from '../util/language_pack_default'; import { defaultPack } from '../util/language_pack_default';
import { availablePacks } from '../util/language_pack_context'; import { availablePacks } from '../util/language_pack_context';
@ -149,8 +149,8 @@ THE SOFTWARE.
Global, Global,
Setup, Setup,
NginxPrism, NginxPrism,
'YamlPrism': () => import('./prism/yaml'), YamlPrism: () => import('./prism/yaml'),
'DockerPrism': () => import('./prism/docker'), DockerPrism: () => import('./prism/docker'),
}, },
data() { data() {
return { return {
@ -171,9 +171,10 @@ THE SOFTWARE.
splitColumn: false, splitColumn: false,
confWatcherWaiting: false, confWatcherWaiting: false,
confFilesPrevious: {}, confFilesPrevious: {},
confFilesOutput: {}, confFilesOutput: [],
languageLoading: false, languageLoading: false,
languagePrevious: defaultPack, languagePrevious: defaultPack,
interactiveEvents: false,
}; };
}, },
computed: { computed: {
@ -214,8 +215,12 @@ THE SOFTWARE.
}, },
'$data.global.app.lang': { '$data.global.app.lang': {
handler(data) { handler(data) {
// Lock out the dropdown
this.$data.languageLoading = true; this.$data.languageLoading = true;
// Store if we should fire the event when this is done loading
const interactive = this.$data.interactiveEvents;
// Ensure valid pack // Ensure valid pack
if (!availablePacks.includes(data.value)) data.computed = data.default; if (!availablePacks.includes(data.value)) data.computed = data.default;
@ -227,7 +232,7 @@ THE SOFTWARE.
this.$data.languageLoading = false; this.$data.languageLoading = false;
// Analytics // Analytics
analytics(`set_language_${toSep(data.computed, '_')}`, 'Language'); this.languageSetEvent(!interactive);
}).catch((err) => { }).catch((err) => {
// Error // Error
console.log('Failed to set language to', data.computed); console.log('Failed to set language to', data.computed);
@ -255,8 +260,10 @@ THE SOFTWARE.
if (language) this.lang = language; if (language) this.lang = language;
} }
// Send an initial GA event for column mode // Initial analytics events
this.splitColumnEvent(true); this.splitColumnEvent(true);
for (let i = 0; i < this.activeDomains.length; i++) this.addSiteEvent(i + 1, true);
this.$data.interactiveEvents = true;
}, },
methods: { methods: {
changes(index) { changes(index) {
@ -285,15 +292,16 @@ THE SOFTWARE.
this.$data.domains.push(data); this.$data.domains.push(data);
this.$data.active = this.$data.domains.length - 1; this.$data.active = this.$data.domains.length - 1;
// GA // Analytics
analytics('add_site', 'Sites', undefined, this.activeDomains.length); this.addSiteEvent(this.activeDomains.length);
}, },
remove(index) { remove(index) {
const name = this.$data.domains[index].server.domain.computed;
this.$set(this.$data.domains, index, null); this.$set(this.$data.domains, index, null);
if (this.$data.active === index) this.$data.active = this.$data.domains.findIndex(d => d !== null); if (this.$data.active === index) this.$data.active = this.$data.domains.findIndex(d => d !== null);
// GA // Analytics
analytics('remove_site', 'Sites', undefined, this.activeDomains.length); this.removeSiteEvent(this.activeDomains.length, name);
}, },
checkChange(oldConf) { checkChange(oldConf) {
// If nothing has changed for a tick, we can use the config files // If nothing has changed for a tick, we can use the config files
@ -319,7 +327,7 @@ THE SOFTWARE.
const diffConf = diff(newConf, oldConf, { const diffConf = diff(newConf, oldConf, {
highlightFunction: value => `<mark>${value}</mark>`, highlightFunction: value => `<mark>${value}</mark>`,
}); });
this.$data.confFilesOutput = Object.values(diffConf).map(({ name, content }) => { this.$data.confFilesOutput = Object.entries(diffConf).map(([ file, { name, content } ]) => {
const diffName = name.filter(x => !x.removed).map(x => x.value).join(''); const diffName = name.filter(x => !x.removed).map(x => x.value).join('');
const confName = `${escape(this.$data.global.nginx.nginxConfigDirectory.computed)}/${diffName}`; const confName = `${escape(this.$data.global.nginx.nginxConfigDirectory.computed)}/${diffName}`;
const diffContent = content.filter(x => !x.removed).map(x => x.value).join(''); const diffContent = content.filter(x => !x.removed).map(x => x.value).join('');
@ -328,6 +336,7 @@ THE SOFTWARE.
confName, confName,
diffContent, diffContent,
`${sha2_256(confName)}-${sha2_256(diffContent)}`, `${sha2_256(confName)}-${sha2_256(diffContent)}`,
file,
]; ];
}); });
} catch (e) { } catch (e) {
@ -339,6 +348,7 @@ THE SOFTWARE.
confName, confName,
content, content,
`${sha2_256(confName)}-${sha2_256(content)}`, `${sha2_256(confName)}-${sha2_256(content)}`,
name,
]; ];
}); });
} }
@ -351,7 +361,42 @@ THE SOFTWARE.
this.splitColumnEvent(); this.splitColumnEvent();
}, },
splitColumnEvent(nonInteraction = false) { splitColumnEvent(nonInteraction = false) {
analytics('toggle_split_column', 'Button', undefined, Number(this.$data.splitColumn), nonInteraction); analytics({
category: 'Split column',
action: this.$data.splitColumn ? 'Enabled' : 'Disabled',
nonInteraction,
});
},
languageSetEvent(nonInteraction = false) {
analytics({
category: 'Language',
action: 'Set',
label: this.$data.global.app.lang.computed,
nonInteraction,
});
},
addSiteEvent(count, nonInteraction = false) {
analytics({
category: 'Site',
action: 'Added',
value: count,
nonInteraction,
});
},
removeSiteEvent(count, name) {
analytics({
category: 'Site',
action: 'Removed',
label: name,
value: count,
});
},
codeCopiedEvent(file) {
analytics({
category: 'Config files',
action: 'Code snippet copied',
label: file,
});
}, },
getPrismComponent(confName) { getPrismComponent(confName) {
switch (confName) { switch (confName) {

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -34,7 +34,7 @@ THE SOFTWARE.
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li v-for="tab in tabs" :class="tabClass(tab.key)"> <li v-for="tab in tabs" :class="tabClass(tab.key)">
<a @click="active = tab.key">{{ $t(tab.display) }}{{ changes(tab.key) }}</a> <a @click="showTab(tab.key)">{{ $t(tab.display) }}{{ changes(tab.key) }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -48,10 +48,10 @@ THE SOFTWARE.
></component> ></component>
<div class="navigation-buttons"> <div class="navigation-buttons">
<a v-if="previousTab !== false" class="button is-mini" @click="active = previousTab"> <a v-if="previousTab !== false" class="button is-mini" @click="showPreviousTab">
<i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span> <i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span>
</a> </a>
<a v-if="nextTab !== false" class="button is-primary is-mini" @click="active = nextTab"> <a v-if="nextTab !== false" class="button is-primary is-mini" @click="showNextTab">
<span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i> <span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i>
</a> </a>
</div> </div>
@ -60,6 +60,7 @@ THE SOFTWARE.
</template> </template>
<script> <script>
import analytics from '../util/analytics';
import isChanged from '../util/is_changed'; import isChanged from '../util/is_changed';
import Presets from './domain_sections/presets'; import Presets from './domain_sections/presets';
import * as Sections from './domain_sections'; import * as Sections from './domain_sections';
@ -127,6 +128,39 @@ THE SOFTWARE.
if (tabs.indexOf(tab) < tabs.indexOf(this.$data.active)) classes.push('is-before'); if (tabs.indexOf(tab) < tabs.indexOf(this.$data.active)) classes.push('is-before');
return classes.join(' '); return classes.join(' ');
}, },
showTab(target) {
// Analytics
analytics({
category: 'Site',
action: 'Tab clicked',
label: `${this.$data.active}, ${target}`,
});
// Go!
this.$data.active = target;
},
showPreviousTab() {
// Analytics
analytics({
category: 'Site',
action: 'Back clicked',
label: `${this.$data.active}, ${this.previousTab}`,
});
// Go!
this.$data.active = this.previousTab;
},
showNextTab() {
// Analytics
analytics({
category: 'Site',
action: 'Next clicked',
label: `${this.$data.active}, ${this.nextTab}`,
});
// Go!
this.$data.active = this.nextTab;
},
}, },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -56,7 +56,7 @@ THE SOFTWARE.
</div> </div>
</template> </template>
<div class="control" v-if="incorrectEnding"> <div v-if="incorrectEnding" class="control">
<label class="text message is-warning"> <label class="text message is-warning">
<span class="message-body"> <span class="message-body">
{{ $t('templates.domainSections.onion.onionLocationExpectedToEndWithOnion') }} {{ $t('templates.domainSections.onion.onionLocationExpectedToEndWithOnion') }}

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -56,7 +56,6 @@ THE SOFTWARE.
import delegatedFromDefaults from '../../util/delegated_from_defaults'; import delegatedFromDefaults from '../../util/delegated_from_defaults';
import computedFromDefaults from '../../util/computed_from_defaults'; import computedFromDefaults from '../../util/computed_from_defaults';
import analytics from '../../util/analytics'; import analytics from '../../util/analytics';
import camelToSnake from '../../util/camel_to_snake';
const defaults = { const defaults = {
frontend: { frontend: {
@ -208,7 +207,7 @@ THE SOFTWARE.
setPreset(key) { setPreset(key) {
// Set that we're using this preset // Set that we're using this preset
Object.keys(this.$props.data).forEach(preset => this[preset] = preset === key); Object.keys(this.$props.data).forEach(preset => this[preset] = preset === key);
analytics(`apply_${camelToSnake(key)}`, 'Presets'); this.presetEvent(key, this.interacted);
// Restore some specific defaults first // Restore some specific defaults first
this.$parent.resetValue('server', 'domain'); this.$parent.resetValue('server', 'domain');
@ -271,6 +270,13 @@ THE SOFTWARE.
break; break;
} }
}, },
presetEvent(name, overwrite = false) {
analytics({
category: 'Preset',
action: overwrite ? 'Overwritten' : 'Applied', // TODO: Is overwritten the best word here?
label: name,
});
},
toggleCollapse() { toggleCollapse() {
if (this.interacted) { if (this.interacted) {
this.expanded = !this.expanded; this.expanded = !this.expanded;

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -29,7 +29,7 @@ THE SOFTWARE.
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li v-for="tab in tabs" :class="tabClass(tab.key)"> <li v-for="tab in tabs" :class="tabClass(tab.key)">
<a @click="active = tab.key">{{ $t(tab.display) }}{{ changes(tab.key) }}</a> <a @click="showTab(tab.key)">{{ $t(tab.display) }}{{ changes(tab.key) }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -43,10 +43,10 @@ THE SOFTWARE.
></component> ></component>
<div class="navigation-buttons"> <div class="navigation-buttons">
<a v-if="previousTab !== false" class="button is-mini" @click="active = previousTab"> <a v-if="previousTab !== false" class="button is-mini" @click="showPreviousTab">
<i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span> <i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span>
</a> </a>
<a v-if="nextTab !== false" class="button is-primary is-mini" @click="active = nextTab"> <a v-if="nextTab !== false" class="button is-primary is-mini" @click="showNextTab">
<span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i> <span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i>
</a> </a>
</div> </div>
@ -54,6 +54,7 @@ THE SOFTWARE.
</template> </template>
<script> <script>
import analytics from '../util/analytics';
import isChanged from '../util/is_changed'; import isChanged from '../util/is_changed';
import * as Sections from './global_sections'; import * as Sections from './global_sections';
@ -113,6 +114,39 @@ THE SOFTWARE.
if (tabs.indexOf(tab) < tabs.indexOf(this.$data.active)) classes.push('is-before'); if (tabs.indexOf(tab) < tabs.indexOf(this.$data.active)) classes.push('is-before');
return classes.join(' '); return classes.join(' ');
}, },
showTab(target) {
// Analytics
analytics({
category: 'Global',
action: 'Tab clicked',
label: `${this.$data.active}, ${target}`,
});
// Go!
this.$data.active = target;
},
showPreviousTab() {
// Analytics
analytics({
category: 'Global',
action: 'Back clicked',
label: `${this.$data.active}, ${this.previousTab}`,
});
// Go!
this.$data.active = this.previousTab;
},
showNextTab() {
// Analytics
analytics({
category: 'Global',
action: 'Next clicked',
label: `${this.$data.active}, ${this.nextTab}`,
});
// Go!
this.$data.active = this.nextTab;
},
}, },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -243,7 +243,10 @@ THE SOFTWARE.
this.$t('templates.globalSections.tools.resetGlobalConfig'), this.$t('templates.globalSections.tools.resetGlobalConfig'),
this.$t('templates.globalSections.tools.resetGlobalConfigBody'), this.$t('templates.globalSections.tools.resetGlobalConfigBody'),
() => { () => {
analytics('reset_global', 'Reset'); // Analytics
this.resetGlobalEvent();
// Do the reset
Object.values(this.$parent.$props.data).forEach(category => { Object.values(this.$parent.$props.data).forEach(category => {
Object.values(category).forEach(property => { Object.values(category).forEach(property => {
property.value = property.default; property.value = property.default;
@ -264,7 +267,10 @@ THE SOFTWARE.
${domain.server.domain.computed} ${domain.server.domain.computed}
${this.$t('templates.globalSections.tools.domain')}`, ${this.$t('templates.globalSections.tools.domain')}`,
() => { () => {
analytics('reset_domain', 'Reset', domain.server.domain.computed); // Analytics
this.resetDomainEvent(domain.server.domain.computed);
// Do the reset
this.doResetDomain(domain); this.doResetDomain(domain);
}, },
); );
@ -280,13 +286,10 @@ THE SOFTWARE.
${domain.server.domain.computed} ${domain.server.domain.computed}
${this.$t('templates.globalSections.tools.domainConfiguration')}`, ${this.$t('templates.globalSections.tools.domainConfiguration')}`,
() => { () => {
analytics( // Analytics
'remove_domain', this.removeDomainEvent(domain.server.domain.computed);
'Remove',
domain.server.domain.computed,
this.$parent.$parent.activeDomains.length - 1,
);
// Do the removal
this.doRemoveDomain(index); this.doRemoveDomain(index);
}, },
); );
@ -296,13 +299,13 @@ THE SOFTWARE.
this.$t('templates.globalSections.tools.resetAllDomainsConfig'), this.$t('templates.globalSections.tools.resetAllDomainsConfig'),
this.$t('templates.globalSections.tools.resetAllDomainsConfigBody'), this.$t('templates.globalSections.tools.resetAllDomainsConfigBody'),
() => { () => {
analytics( // Analytics
'reset_all', this.resetDomainsEvent(
'Reset', this.$parent.$parent.activeDomains.map(x => x[0].server.domain.computed),
this.$parent.$parent.activeDomains.map(x => x[0].server.domain.computed).join(','),
this.$parent.$parent.activeDomains.length, this.$parent.$parent.activeDomains.length,
); );
// Do the reset
for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) { for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) {
this.doResetDomain(this.$parent.$parent.$data.domains[i]); this.doResetDomain(this.$parent.$parent.$data.domains[i]);
} }
@ -314,19 +317,62 @@ THE SOFTWARE.
this.$t('templates.globalSections.tools.removeAllDomains'), this.$t('templates.globalSections.tools.removeAllDomains'),
this.$t('templates.globalSections.tools.removeAllDomainsBody'), this.$t('templates.globalSections.tools.removeAllDomainsBody'),
() => { () => {
analytics( // Analytics
'remove_all', this.removeDomainsEvent(
'Remove', this.$parent.$parent.activeDomains.map(x => x[0].server.domain.computed),
this.$parent.$parent.activeDomains.map(x => x[0].server.domain.computed).join(','),
this.$parent.$parent.activeDomains.length, this.$parent.$parent.activeDomains.length,
); );
// Do the removal
for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) { for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++) {
this.doRemoveDomain(i); this.doRemoveDomain(i);
} }
}, },
); );
}, },
resetGlobalEvent() {
analytics({
category: 'Tools',
action: 'Global settings reset',
});
},
resetDomainEvent(name) {
analytics({
category: 'Tools',
action: 'Site reset',
label: name,
});
},
removeDomainEvent(name) {
analytics({
category: 'Tools',
action: 'Removed site',
label: name,
});
// Also fire the general site removal event
this.$parent.$parent.removeSiteEvent(this.$parent.$parent.activeDomains.length - 1, name);
},
resetDomainsEvent(names, count) {
analytics({
category: 'Tools',
action: 'All sites reset',
label: names.join(', '),
value: count,
});
},
removeDomainsEvent(names, count) {
analytics({
category: 'Tools',
action: 'All sites removed',
label: names.join(', '),
value: count,
});
// Also fire the general site removal event
for (let i = 0; i < this.$parent.$parent.$data.domains.length; i++)
this.$parent.$parent.removeSiteEvent(this.$parent.$parent.activeDomains.length - i - 1, names[i]);
},
select(event) { select(event) {
event.target.setSelectionRange(0, event.target.value.length); event.target.setSelectionRange(0, event.target.value.length);
}, },

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -25,7 +25,7 @@ THE SOFTWARE.
--> -->
<template> <template>
<div> <div @copied="copied">
<pre><code class="language-bash">{{ cmd }}</code></pre> <pre><code class="language-bash">{{ cmd }}</code></pre>
</div> </div>
</template> </template>
@ -42,5 +42,10 @@ THE SOFTWARE.
console.info(`Highlighting ${this.$props.cmd}...`); console.info(`Highlighting ${this.$props.cmd}...`);
Prism.highlightAllUnder(this.$el); Prism.highlightAllUnder(this.$el);
}, },
methods: {
copied(event) {
this.$emit('copied', event.detail.text);
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -25,7 +25,7 @@ THE SOFTWARE.
--> -->
<template> <template>
<div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`"> <div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`" @copied="copied">
<h3 v-html="name"></h3> <h3 v-html="name"></h3>
<pre><code class="language-docker" v-html="conf"></code></pre> <pre><code class="language-docker" v-html="conf"></code></pre>
</div> </div>
@ -46,5 +46,10 @@ THE SOFTWARE.
console.info(`Highlighting ${this.$props.name}...`); console.info(`Highlighting ${this.$props.name}...`);
Prism.highlightAllUnder(this.$el); Prism.highlightAllUnder(this.$el);
}, },
methods: {
copied(event) {
this.$emit('copied', event.detail.text);
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -25,7 +25,7 @@ THE SOFTWARE.
--> -->
<template> <template>
<div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`"> <div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`" @copied="copied">
<h3 v-html="name"></h3> <h3 v-html="name"></h3>
<pre><code class="language-nginx" v-html="conf"></code></pre> <pre><code class="language-nginx" v-html="conf"></code></pre>
</div> </div>
@ -45,5 +45,10 @@ THE SOFTWARE.
console.info(`Highlighting ${this.$props.name}...`); console.info(`Highlighting ${this.$props.name}...`);
Prism.highlightAllUnder(this.$el); Prism.highlightAllUnder(this.$el);
}, },
methods: {
copied(event) {
this.$emit('copied', event.detail.text);
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -25,7 +25,7 @@ THE SOFTWARE.
--> -->
<template> <template>
<div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`"> <div :class="`column ${half ? 'is-half' : 'is-full'} is-full-mobile is-full-tablet`" @copied="copied">
<h3 v-html="name"></h3> <h3 v-html="name"></h3>
<pre><code class="language-yaml" v-html="conf"></code></pre> <pre><code class="language-yaml" v-html="conf"></code></pre>
</div> </div>
@ -46,5 +46,10 @@ THE SOFTWARE.
console.info(`Highlighting ${this.$props.name}...`); console.info(`Highlighting ${this.$props.name}...`);
Prism.highlightAllUnder(this.$el); Prism.highlightAllUnder(this.$el);
}, },
methods: {
copied(event) {
this.$emit('copied', event.detail.text);
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -30,7 +30,7 @@ THE SOFTWARE.
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li v-for="tab in tabs" :class="tabClass(tab.key)"> <li v-for="tab in tabs" :class="tabClass(tab.key)">
<a @click="active = tab.key">{{ $t(tab.display) }}</a> <a @click="showTab(tab.key)">{{ $t(tab.display) }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -44,10 +44,10 @@ THE SOFTWARE.
></component> ></component>
<div class="navigation-buttons"> <div class="navigation-buttons">
<a v-if="previousTab !== false" class="button is-mini" @click="active = previousTab"> <a v-if="previousTab !== false" class="button is-mini" @click="showPreviousTab">
<i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span> <i class="fas fa-long-arrow-alt-left"></i> <span>{{ $t('common.back') }}</span>
</a> </a>
<a v-if="nextTab !== false" class="button is-primary is-mini" @click="active = nextTab"> <a v-if="nextTab !== false" class="button is-primary is-mini" @click="showNextTab">
<span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i> <span>{{ $t('common.next') }}</span> <i class="fas fa-long-arrow-alt-right"></i>
</a> </a>
</div> </div>
@ -92,6 +92,9 @@ THE SOFTWARE.
if (index >= 0) return tabs[index]; if (index >= 0) return tabs[index];
return false; return false;
}, },
domainCount() {
return this.$props.data.domains.filter(d => d !== null).length;
},
tarName() { tarName() {
const domains = this.$props.data.domains.filter(d => d !== null).map(d => d.server.domain.computed); const domains = this.$props.data.domains.filter(d => d !== null).map(d => d.server.domain.computed);
return `nginxconfig.io-${domains.join(',')}.tar.gz`; return `nginxconfig.io-${domains.join(',')}.tar.gz`;
@ -123,11 +126,27 @@ THE SOFTWARE.
return new Tar(data).gz(); return new Tar(data).gz();
}, },
downloadTar() { downloadTar() {
analytics('download_tar', 'Download', this.tarName); // Analytics
analytics({
category: 'Setup',
action: 'Downloaded tar file',
label: this.tarName,
value: this.domainCount,
});
// Do tar generation
this.tarContents().download(this.tarName); this.tarContents().download(this.tarName);
}, },
copyTar() { copyTar() {
analytics('download_base64', 'Download', this.tarName); // Analytics
analytics({
category: 'Setup',
action: 'Copied base64 tar',
label: this.tarName,
value: this.domainCount,
});
// Do tar generation
const path = `${this.$props.data.global.nginx.nginxConfigDirectory.computed}/${this.tarName}`; const path = `${this.$props.data.global.nginx.nginxConfigDirectory.computed}/${this.tarName}`;
return this.tarContents().base64(path); return this.tarContents().base64(path);
}, },
@ -155,6 +174,39 @@ THE SOFTWARE.
resetText(); resetText();
}); });
}, },
showTab(target) {
// Analytics
analytics({
category: 'Setup',
action: 'Tab clicked',
label: `${this.$data.active}, ${target}`,
});
// Go!
this.$data.active = target;
},
showPreviousTab() {
// Analytics
analytics({
category: 'Setup',
action: 'Back clicked',
label: `${this.$data.active}, ${this.previousTab}`,
});
// Go!
this.$data.active = this.previousTab;
},
showNextTab() {
// Analytics
analytics({
category: 'Setup',
action: 'Next clicked',
label: `${this.$data.active}, ${this.nextTab}`,
});
// Go!
this.$data.active = this.nextTab;
},
}, },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -34,6 +34,7 @@ THE SOFTWARE.
</p> </p>
<BashPrism :key="sitesAvailable" <BashPrism :key="sitesAvailable"
:cmd="`sed -i -r 's/(listen .*443)/\\1;#/g; s/(ssl_(certificate|certificate_key|trusted_certificate) )/#;#\\1/g' ${sitesAvailable}`" :cmd="`sed -i -r 's/(listen .*443)/\\1;#/g; s/(ssl_(certificate|certificate_key|trusted_certificate) )/#;#\\1/g' ${sitesAvailable}`"
@copied="codeCopiedEvent('Disable ssl directives')"
></BashPrism> ></BashPrism>
</li> </li>
@ -42,7 +43,9 @@ THE SOFTWARE.
{{ $t('templates.setupSections.certbot.reloadYourNginxServer') }} {{ $t('templates.setupSections.certbot.reloadYourNginxServer') }}
<br /> <br />
</p> </p>
<BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"></BashPrism> <BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"
@copied="codeCopiedEvent('Reload nginx')"
></BashPrism>
</li> </li>
<li> <li>
@ -50,7 +53,10 @@ THE SOFTWARE.
{{ $t('templates.setupSections.certbot.obtainSslCertificatesFromLetsEncrypt') }} {{ $t('templates.setupSections.certbot.obtainSslCertificatesFromLetsEncrypt') }}
<br /> <br />
</p> </p>
<BashPrism :key="certbotCmds" :cmd="certbotCmds"></BashPrism> <BashPrism :key="certbotCmds"
:cmd="certbotCmds"
@copied="codeCopiedEvent('Obtain certificates using certbot')"
></BashPrism>
</li> </li>
<li> <li>
@ -58,7 +64,10 @@ THE SOFTWARE.
{{ $t('templates.setupSections.certbot.uncommentSslDirectivesInConfiguration') }} {{ $t('templates.setupSections.certbot.uncommentSslDirectivesInConfiguration') }}
<br /> <br />
</p> </p>
<BashPrism :key="sitesAvailable" :cmd="`sed -i -r 's/#?;#//g' ${sitesAvailable}`"></BashPrism> <BashPrism :key="sitesAvailable"
:cmd="`sed -i -r 's/#?;#//g' ${sitesAvailable}`"
@copied="codeCopiedEvent('Enable ssl directives')"
></BashPrism>
</li> </li>
<li> <li>
@ -66,7 +75,9 @@ THE SOFTWARE.
{{ $t('templates.setupSections.certbot.reloadYourNginxServer') }} {{ $t('templates.setupSections.certbot.reloadYourNginxServer') }}
<br /> <br />
</p> </p>
<BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"></BashPrism> <BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"
@copied="codeCopiedEvent('Reload nginx (2)')"
></BashPrism>
</li> </li>
<li> <li>
@ -74,8 +85,12 @@ THE SOFTWARE.
{{ $t('templates.setupSections.certbot.configureCertbotToReloadNginxOnCertificateRenewal') }} {{ $t('templates.setupSections.certbot.configureCertbotToReloadNginxOnCertificateRenewal') }}
<br /> <br />
</p> </p>
<BashPrism cmd="echo -e '#!/bin/bash\nnginx -t && systemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh"></BashPrism> <BashPrism cmd="echo -e '#!/bin/bash\nnginx -t && systemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh"
<BashPrism cmd="sudo chmod a+x /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh"></BashPrism> @copied="codeCopiedEvent('Create nginx auto-restart on renewal')"
></BashPrism>
<BashPrism cmd="sudo chmod a+x /etc/letsencrypt/renewal-hooks/post/nginx-reload.sh"
@copied="codeCopiedEvent('Enable execution of auto-restart')"
></BashPrism>
</li> </li>
</ol> </ol>
@ -95,6 +110,7 @@ THE SOFTWARE.
<script> <script>
import BashPrism from '../prism/bash'; import BashPrism from '../prism/bash';
import analytics from '../../util/analytics';
export default { export default {
name: 'SetupCertbot', name: 'SetupCertbot',
@ -144,5 +160,14 @@ THE SOFTWARE.
)).join('\n'); )).join('\n');
}, },
}, },
methods: {
codeCopiedEvent(step) {
analytics({
category: 'Setup',
action: 'Code snippet copied',
label: `certbot: ${step}`,
});
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -52,6 +52,7 @@ THE SOFTWARE.
<br /> <br />
<BashPrism :key="$props.data.global.nginx.nginxConfigDirectory.computed" <BashPrism :key="$props.data.global.nginx.nginxConfigDirectory.computed"
:cmd="`cd ${$props.data.global.nginx.nginxConfigDirectory.computed}`" :cmd="`cd ${$props.data.global.nginx.nginxConfigDirectory.computed}`"
@copied="codeCopiedEvent('Navigate to nginx config directory')"
></BashPrism> ></BashPrism>
</p> </p>
</li> </li>
@ -60,7 +61,9 @@ THE SOFTWARE.
<p> <p>
<span v-html="$t('templates.setupSections.download.createABackupOfYourCurrentNginxConfiguration')"></span> <span v-html="$t('templates.setupSections.download.createABackupOfYourCurrentNginxConfiguration')"></span>
<br /> <br />
<BashPrism cmd="tar -czvf nginx_$(date +'%F_%H-%M-%S').tar.gz nginx.conf sites-available/ sites-enabled/ nginxconfig.io/"></BashPrism> <BashPrism cmd="tar -czvf nginx_$(date +'%F_%H-%M-%S').tar.gz nginx.conf sites-available/ sites-enabled/ nginxconfig.io/"
@copied="codeCopiedEvent('Create nginx config backup tar')"
></BashPrism>
</p> </p>
</li> </li>
@ -68,7 +71,10 @@ THE SOFTWARE.
<p> <p>
<span v-html="$t('templates.setupSections.download.extractTheNewCompressedConfigurationArchiveUsingTar')"></span> <span v-html="$t('templates.setupSections.download.extractTheNewCompressedConfigurationArchiveUsingTar')"></span>
<br /> <br />
<BashPrism :key="$parent.tarName" :cmd="`tar -xzvf ${$parent.tarName}`"></BashPrism> <BashPrism :key="$parent.tarName"
:cmd="`tar -xzvf ${$parent.tarName}`"
@copied="codeCopiedEvent('Extract new nginx config tar')"
></BashPrism>
</p> </p>
</li> </li>
</ol> </ol>
@ -77,6 +83,7 @@ THE SOFTWARE.
<script> <script>
import BashPrism from '../prism/bash'; import BashPrism from '../prism/bash';
import analytics from '../../util/analytics';
export default { export default {
name: 'SetupDownload', name: 'SetupDownload',
@ -91,5 +98,14 @@ THE SOFTWARE.
mounted() { mounted() {
this.$parent.setupCopy(this.$refs.copyTar); this.$parent.setupCopy(this.$refs.copyTar);
}, },
methods: {
codeCopiedEvent(step) {
analytics({
category: 'Setup',
action: 'Code snippet copied',
label: `download: ${step}`,
});
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -32,13 +32,16 @@ THE SOFTWARE.
<p> <p>
{{ $t('templates.setupSections.goLive.reloadNginxToLoadInYourNewConfiguration') }} {{ $t('templates.setupSections.goLive.reloadNginxToLoadInYourNewConfiguration') }}
<br /> <br />
<BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"></BashPrism> <BashPrism cmd="sudo nginx -t && sudo systemctl reload nginx"
@copied="codeCopiedEvent('Reload nginx')"
></BashPrism>
</p> </p>
</div> </div>
</template> </template>
<script> <script>
import BashPrism from '../prism/bash'; import BashPrism from '../prism/bash';
import analytics from '../../util/analytics';
export default { export default {
name: 'SetupGoLive', name: 'SetupGoLive',
@ -50,5 +53,14 @@ THE SOFTWARE.
props: { props: {
data: Object, data: Object,
}, },
methods: {
codeCopiedEvent(step) {
analytics({
category: 'Setup',
action: 'Code snippet copied',
label: `goLive: ${step}`,
});
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<!-- <!--
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -33,6 +33,7 @@ THE SOFTWARE.
<br /> <br />
<BashPrism :key="`${$props.data.global.nginx.nginxConfigDirectory.computed}-${diffieHellmanValue}`" <BashPrism :key="`${$props.data.global.nginx.nginxConfigDirectory.computed}-${diffieHellmanValue}`"
:cmd="`openssl dhparam -out ${$props.data.global.nginx.nginxConfigDirectory.computed}/dhparam.pem ${diffieHellmanValue}`" :cmd="`openssl dhparam -out ${$props.data.global.nginx.nginxConfigDirectory.computed}/dhparam.pem ${diffieHellmanValue}`"
@copied="codeCopiedEvent('Generate diffie-hellman keys')"
></BashPrism> ></BashPrism>
</p> </p>
</li> </li>
@ -41,9 +42,13 @@ THE SOFTWARE.
<p> <p>
<span v-html="$t('templates.setupSections.ssl.createACommonAcmeChallengeDirectoryForLetsEncrypt')"></span> <span v-html="$t('templates.setupSections.ssl.createACommonAcmeChallengeDirectoryForLetsEncrypt')"></span>
<br /> <br />
<BashPrism :key="letsEncryptDir" :cmd="`mkdir -p ${letsEncryptDir}`"></BashPrism> <BashPrism :key="letsEncryptDir"
:cmd="`mkdir -p ${letsEncryptDir}`"
@copied="codeCopiedEvent('Create let\'s encrypt directory')"
></BashPrism>
<BashPrism :key="`${nginxUser}-${letsEncryptDir}`" <BashPrism :key="`${nginxUser}-${letsEncryptDir}`"
:cmd="`chown ${nginxUser} ${letsEncryptDir}`" :cmd="`chown ${nginxUser} ${letsEncryptDir}`"
@copied="codeCopiedEvent('Set let\'s encrypt directory ownership')"
></BashPrism> ></BashPrism>
</p> </p>
</li> </li>
@ -65,6 +70,7 @@ THE SOFTWARE.
<script> <script>
import BashPrism from '../prism/bash'; import BashPrism from '../prism/bash';
import analytics from '../../util/analytics';
export default { export default {
name: 'SetupSSL', name: 'SetupSSL',
@ -103,5 +109,14 @@ THE SOFTWARE.
return false; return false;
}, },
}, },
methods: {
codeCopiedEvent(step) {
analytics({
category: 'Setup',
action: 'Code snippet copied',
label: `ssl: ${step}`,
});
},
},
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -24,49 +24,234 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
*/ */
export default (action, category, label, value, nonInteraction = false) => { export default ({ category, action, label, value, nonInteraction }) => {
console.info('Analytics event:', { category, action, label, value, nonInteraction });
try { try {
const tracker = window.ga.getAll()[0]; // Google
if (tracker) { window.ga('send', 'event', {
tracker.send({ eventCategory: category,
hitType: 'event', eventAction: action,
eventCategory: category, eventLabel: label,
eventAction: action, eventValue: value,
eventLabel: label, nonInteraction,
eventValue: value, });
nonInteraction,
});
}
} catch (_) { } catch (_) {
// If analytics fail, don't block anything else // If analytics fail, don't block anything else
} }
/*try { try {
// gtag.js // Segment
if (window.gtag) { window.analytics.track(`${category} ${action}`, {
window.gtag('event', action, { label,
event_category: category, value,
event_label: label, nonInteraction,
value, });
});
}
} catch (_) { } catch (_) {
// If analytics fail, don't block anything else // If analytics fail, don't block anything else
}*/ }
/*try {
// analytics.js
if (window.ga) {
window.ga('send', {
hitType: 'event',
eventCategory: category,
eventAction: action,
eventLabel: label,
eventValue: value,
nonInteraction,
});
}
} catch (_) {
// If analytics fail, don't block anything else
}*/
}; };
/*
All analytics events in app:
# Initial language set (from browser or query param)
File: app.vue
Category: 'Language'
Action: 'Set'
Label: language pack name
Non-interaction: true
# User manually changing tool language
File: app.vue
Category: 'Language'
Action: 'Set'
Label: language pack name
Non-interaction: false
# Initial domains set (from query params)
File: app.vue
Category: 'Site'
Action: 'Added'
Value: total number of sites active in tool
Non-interaction: true
# User adding a domain
File: app.vue
Category: 'Site'
Action: 'Added'
Value: total number of sites active in tool
Non-interaction: false
# User removing a domain
File: app.vue
Category: 'Site'
Action: 'Removed'
Label: domain name being removed
Value: total number of sites active in tool after removal
# Initial split column mode (will always be disabled)
File: app.vue
Category: 'Split column'
Action: 'Disabled'
Non-interaction: true
# User changing the column mode
File: app.vue
Category: 'Split column'
Action: 'Disabled' / 'Enabled'
Non-interaction: false
# User applying a preset
File: domain_sections/presets.vue
Category: 'Preset'
Action: 'Applied'
Label: preset internal name
# User applying a preset with previous customisations
File: domain_sections/presets.vue
Category: 'Preset'
Action: 'Overwritten'
Label: preset internal name
# User resetting global settings
File: global_sections/tools.vue
Category: 'Tools'
Action: 'Global settings reset'
# User resetting a domain
File: global_sections/tools.vue
Category: 'Tools'
Action: 'Site reset'
Label: domain name being reset
# User removing a domain in the tools tab
Note: This will also trigger the regular site removal event in app.vue
File: global_sections/tools.vue
Category: 'Tools'
Action: 'Removed site'
Label: domain name being removed
# User resetting all domains
File: global_sections/tools.vue
Category: 'Tools'
Action: 'All sites reset'
Label: comma-separated list of domain names being reset
Value: total number of domains being reset
# User removing all domains
Note: This will also trigger the regular site removal event in app.vue for each domain removed
File: global_sections/tools.vue
Category: 'Tools'
Action: 'All sites removed'
Label: comma-separated list of domain names being removed
Value: total number of domains being removed
# User clicking a tab in global settings
File: global.vue
Category: 'Global'
Action: 'Tab clicked'
Label: from tab, to tab
# User clicking back in global settings
File: global.vue
Category: 'Global'
Action: 'Back clicked'
Label: from tab, to tab
# User clicking next in global settings
File: global.vue
Category: 'Global'
Action: 'Next clicked'
Label: from tab, to tab
# User clicking a tab in domain settings
File: domain.vue
Category: 'Site'
Action: 'Tab clicked'
Label: from tab, to tab
# User clicking back in domain settings
File: domain.vue
Category: 'Site'
Action: 'Back clicked'
Label: from tab, to tab
# User clicking next in domain settings
File: domain.vue
Category: 'Site'
Action: 'Next clicked'
Label: from tab, to tab
# User clicking a tab in setup
File: setup.vue
Category: 'Setup'
Action: 'Tab clicked'
Label: from tab, to tab
# User clicking back in setup
File: setup.vue
Category: 'Setup'
Action: 'Back clicked'
Label: from tab, to tab
# User clicking next in setup
File: setup.vue
Category: 'Setup'
Action: 'Next clicked'
Label: from tab, to tab
# User downloading the config
File: setup.vue
Category: 'Setup'
Action: 'Downloaded tar file'
Label: name of the tar file (incl. domain names)
Value: total number of active domains
# User copying the base64 config
File: setup.vue
Category: 'Setup'
Action: 'Copied base64 tar'
Label: name of the tar file (incl. domain names)
Value: total number of active domains
# User copying a code snippet in setup
File: setup.vue
Category: 'Setup'
Action: 'Code snippet copied'
Label: tab name: a summary of the code snippet
# User copying a config file
File: app.vue
Category: 'Config files'
Action: 'Code snippet copied'
Label: name of file without nginx directory
*/

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2020 DigitalOcean Copyright 2021 DigitalOcean
This code is licensed under the MIT License. This code is licensed under the MIT License.
You may obtain a copy of the License at You may obtain a copy of the License at
@ -24,10 +24,57 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
*/ */
import 'prismjs'; import Clipboard from 'clipboard';
import Prism from 'prismjs';
import 'prismjs/components/prism-nginx'; import 'prismjs/components/prism-nginx';
import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-bash';
import 'prismjs/plugins/keep-markup/prism-keep-markup'; import 'prismjs/plugins/keep-markup/prism-keep-markup';
import 'prismjs/plugins/toolbar/prism-toolbar'; import 'prismjs/plugins/toolbar/prism-toolbar';
import 'prismjs/plugins/toolbar/prism-toolbar.css'; import 'prismjs/plugins/toolbar/prism-toolbar.css';
import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard';
// Custom copy to clipboard (based on the Prism one)
const copyToClipboard = () => {
if (!Prism.plugins.toolbar) {
console.warn('Copy to Clipboard loaded before Toolbar.');
return;
}
Prism.plugins.toolbar.registerButton('copy-to-clipboard', env => {
const linkCopy = document.createElement('button');
linkCopy.textContent = 'Copy';
const element = env.element;
const clip = new Clipboard(linkCopy, {
'text': () => element.textContent,
});
const resetText = () => {
setTimeout(() => {
linkCopy.textContent = 'Copy';
}, 5000);
};
const emitEvent = () => {
linkCopy.dispatchEvent(new CustomEvent('copied', {
bubbles: true,
detail: { text: element.textContent },
}));
};
clip.on('success', () => {
linkCopy.textContent = 'Copied!';
emitEvent();
resetText();
});
clip.on('error', () => {
const isMac = navigator.platform.includes('Mac');
linkCopy.textContent = `Press ${isMac ? 'Cmd' : 'Ctrl'}+C to copy`;
resetText();
});
return linkCopy;
});
};
copyToClipboard();