mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
814 lines
51 KiB
HTML
814 lines
51 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
{{ template "head.tpl.html" . }}
|
|
<script src="assets/timezones.js"></script>
|
|
|
|
<script type="module">
|
|
const defaultTab = 'account'
|
|
|
|
PetiteVue.createApp({
|
|
$delimiters: ['${', '}'],
|
|
activeTab: defaultTab,
|
|
updateTab() {
|
|
this.activeTab = window.location.hash.slice(1) || defaultTab
|
|
},
|
|
isActive(tab) {
|
|
return this.activeTab === tab
|
|
},
|
|
mounted() {
|
|
this.updateTab()
|
|
window.addEventListener('hashchange', () => this.updateTab())
|
|
}
|
|
}).mount()
|
|
</script>
|
|
|
|
<body class="bg-gray-900 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
|
|
|
<style>
|
|
.inline-bullet-list li a {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.inline-bullet-list li:after {
|
|
content: "•";
|
|
}
|
|
|
|
.inline-bullet-list li:last-child:after {
|
|
content: "";
|
|
}
|
|
</style>
|
|
|
|
{{ template "menu-main.tpl.html" . }}
|
|
|
|
{{ template "alerts.tpl.html" . }}
|
|
|
|
<main class="mt-10 flex-grow flex justify-center w-full" v-scope @vue:mounted="mounted">
|
|
<div class="flex flex-col flex-grow mt-10">
|
|
<h1 class="font-semibold text-3xl text-white m-0 mb-4">Settings</h1>
|
|
|
|
<ul class="flex space-x-4 mb-16 text-gray-600">
|
|
<li class="font-semibold text-2xl" v-bind:class="{ 'text-gray-300': isActive('account'), 'hover:text-gray-500': !isActive('account') }">
|
|
<a href="settings#account" @click="updateTab">Account</a>
|
|
</li>
|
|
<li class="font-semibold text-2xl" v-bind:class="{ 'text-gray-300': isActive('data'), 'hover:text-gray-500': !isActive('data') }">
|
|
<a href="settings#data" @click="updateTab">Data</a>
|
|
</li>
|
|
<li class="font-semibold text-2xl" v-bind:class="{ 'text-gray-300': isActive('permissions'), 'hover:text-gray-500': !isActive('permissions') }">
|
|
<a href="settings#permissions" @click="updateTab">Permissions</a>
|
|
</li>
|
|
<li class="font-semibold text-2xl" v-bind:class="{ 'text-gray-300': isActive('integrations'), 'hover:text-gray-500': !isActive('integrations') }">
|
|
<a href="settings#integrations" @click="updateTab">Integrations</a>
|
|
</li>
|
|
<li class="font-semibold text-2xl" v-bind:class="{ 'text-gray-300': isActive('danger_zone'), 'hover:text-gray-500': !isActive('danger_zone') }">
|
|
<a href="settings#danger_zone" @click="updateTab">Danger Zone</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div id="account" class="tab flex flex-col space-y-4" v-if="isActive('account')">
|
|
<!-- Account Settings -->
|
|
<form action="" method="post" class="w-3/4">
|
|
<input type="hidden" name="action" value="update_user">
|
|
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="select-timezone">Time Zone</label>
|
|
<span class="block text-sm text-gray-600">Time Zone, which you are located in. Relevant for displaying daily statistics.</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<select name="location" id="select-timezone"
|
|
class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full cursor-pointer">
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="email">E-Mail Address</label>
|
|
<span class="block text-sm text-gray-600">Optional in general, but required for weekly reports and for resetting your password.</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<input class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full"
|
|
type="email" id="email"
|
|
name="email" placeholder="Enter your e-mail address"
|
|
value="{{ .User.Email }}">
|
|
</div>
|
|
</div>
|
|
|
|
{{ if .User.Email }}
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="reports_weekly">Weekly E-Mail Reports</label>
|
|
<span class="block text-sm text-gray-600">Opt in to receive a summary of your coding activity once a week.</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<select autocomplete="off" name="reports_weekly"
|
|
class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full cursor-pointer">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ReportsWeekly }} selected{{ end }}>Disabled</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ReportsWeekly }} selected {{ end }}>Enabled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
|
|
<div class="flex justify-end mt-4">
|
|
<button type="submit" class="py-2 px-4 font-semibold rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="w-3/4">
|
|
<hr class="border-t border-gray-800 my-4">
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<form class="w-3/4" action="" method="post">
|
|
<input type="hidden" name="action" value="change_password">
|
|
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="password_old">Current Password</label>
|
|
<span class="block text-sm text-gray-600">Enter your old password for verification.</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<input class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full"
|
|
type="password" id="password_old"
|
|
name="password_old" placeholder="Old password" minlength="6" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="password_new">New Password</label>
|
|
<span class="block text-sm text-gray-600">Choose a new password. Preferably, it is at least 8 characters long and contains letters, digits and special chars.</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<input class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full"
|
|
type="password" id="password_new"
|
|
name="password_new" placeholder="New password" minlength="6" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex mb-8">
|
|
<div class="w-1/2 mr-4 inline-block">
|
|
<label class="font-semibold text-gray-300" for="password_repeat">Repeat Password</label>
|
|
<span class="block text-sm text-gray-600">Once again ...</span>
|
|
</div>
|
|
<div class="w-1/2 ml-4">
|
|
<input class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 w-full"
|
|
type="password" id="password_repeat"
|
|
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-4">
|
|
<button type="submit" class="py-2 px-4 font-semibold rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="data" class="tab flex flex-col space-y-4" v-if="isActive('data')">
|
|
<!-- Aliases -->
|
|
<div class="w-full">
|
|
<div class="flex mb-8">
|
|
<div class="w-1/3 mr-4 inline-block">
|
|
<span class="font-semibold text-gray-300 text-lg">Aliases</span>
|
|
<p class="block text-sm text-gray-600">You can specify aliases for any type of entity. For instance, you can define a rule, that both "myapp-frontend" and "myapp-backend" are combined under a project called "myapp".</p>
|
|
</div>
|
|
|
|
<div class="w-2/3 ml-4 inline-block">
|
|
{{ if .Aliases }}
|
|
<div class="mb-8">
|
|
<h3 class="inline-block font-semibold text-gray-300">Rules</h3>
|
|
{{ range $i, $alias := .Aliases }}
|
|
<div class="flex items-center">
|
|
<div class="text-gray-300 border-1 w-full inline-block my-1 py-1 text-align text-sm"
|
|
style="line-height: 1.8">
|
|
▸ All <span class="font-semibold">{{ $alias.Type | typeName }}s</span> named
|
|
{{ range $j, $value := $alias.Values }}
|
|
<span class="text-white text-sm bg-gray-900 rounded py-1 px-2 font-semibold text-green-700">{{- $value -}}</span>
|
|
{{ if lt $j (add (len $alias.Values) -2) }}
|
|
<span class="-ml-1">{{- ", " | capitalize -}}</span>
|
|
{{ else if lt $j (add (len $alias.Values) -1) }}
|
|
<span>{{- "or" -}}</span>
|
|
{{ end }}
|
|
{{ end }}
|
|
are mapped to <span class="font-semibold">{{ $alias.Type | typeName }}</span> <span
|
|
class="text-white text-sm bg-gray-900 rounded py-1 px-2 font-semibold text-green-700">{{ $alias.Key }}</span>
|
|
</div>
|
|
<form class="float-right" action="" method="post">
|
|
<input type="hidden" name="action" value="delete_alias">
|
|
<input type="hidden" id="delete_alias_key" name="key" required value="{{ $alias.Key }}">
|
|
<input type="hidden" id="delete_alias_type" name="type" required value="{{ $alias.Type }}">
|
|
<button type="submit" class="py-2 px-4 rounded bg-gray-850 hover:bg-gray-800 text-red-600 text-sm" title="Delete rule">✕</button>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<form action="" method="post">
|
|
<h3 class="inline-block font-semibold text-gray-300">Add Rule</h3>
|
|
|
|
<input type="hidden" name="action" value="add_alias">
|
|
<div class="flex items-center mt-2 w-full text-gray-500 text-sm">
|
|
<span class="mr-2">Map</span>
|
|
<select name="type" id="select-type"
|
|
class="appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 cursor-pointer">
|
|
{{ range $i, $t := entityTypes }}
|
|
<option value="{{ $t }}">{{ $t | typeName | capitalize }}</option>
|
|
{{ end }}
|
|
</select>
|
|
<span class="mx-2">named</span>
|
|
<input class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4"
|
|
type="text" id="alias-value" style="width: 130px;"
|
|
name="value" placeholder="Original name" minlength="1" required>
|
|
<span class="mx-2">to</span>
|
|
<input class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4"
|
|
type="text" id="alias-key" style="width: 100px"
|
|
name="key" placeholder="Replacement" minlength="1" required>
|
|
<div class="flex justify-end ml-4">
|
|
<button type="submit" class="py-2 px-4 font-semibold rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full">
|
|
<hr class="border-t border-gray-800 my-4">
|
|
</div>
|
|
|
|
<!-- Project Labels -->
|
|
<div class="w-full">
|
|
<div class="flex mb-8">
|
|
<div class="w-1/3 mr-4 inline-block">
|
|
<span class="font-semibold text-gray-300 text-lg">Project Labels</span>
|
|
<p class="block text-sm text-gray-600">You can assign labels (aka. tags) to projects to group them together, e.g. by "private" and "work".</p>
|
|
</div>
|
|
|
|
<div class="w-2/3 ml-4 inline-block">
|
|
{{ if .Labels }}
|
|
<div class="mb-8">
|
|
<h3 class="inline-block font-semibold text-gray-300">Labels</h3>
|
|
{{ range $i, $label := .Labels }}
|
|
<div class="flex items-center" action="" method="post">
|
|
<div class="text-gray-500 border-1 w-full border-green-700 inline-block my-1 py-1 text-align text-sm"
|
|
style="line-height: 1.8">
|
|
▸ <span class="font-semibold text-gray-300">{{ $label.Key }}:</span>
|
|
{{ range $j, $value := $label.Values }}
|
|
<form action="" method="post" class="text-white text-xs bg-gray-900 rounded-full py-1 px-2 my-1 inline-flex justify-between items-center space-x-2">
|
|
<input type="hidden" name="action" value="delete_label">
|
|
<input type="hidden" name="key" value="{{ $label.Key }}">
|
|
<input type="hidden" name="value" value="{{ $value }}">
|
|
<span>{{- $value -}}</span>
|
|
<button type="submit" class="bg-gray-800 text-center hover:bg-gray-700 rounded-full w-4 h-4 leading-none text-red-600" title="Delete label">x</button>
|
|
</form>
|
|
{{ if lt $j (add (len $label.Values) -1) }}
|
|
<span class="-ml-1">{{- ", " | capitalize -}}</span>
|
|
{{ end }}
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
<div class="mb-8"></div>
|
|
|
|
{{ if .Projects }}
|
|
<h3 class="inline-block font-semibold text-gray-300">Add Label</h3>
|
|
<form action="" method="post">
|
|
<input type="hidden" name="action" value="add_label">
|
|
<div class="flex flex-col space-y-4">
|
|
<div class="flex items-center mt-2 w-full text-gray-500 text-sm space-x-4">
|
|
<select name="key" id="select-project"
|
|
class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 cursor-pointer">
|
|
{{ range $i, $p := .Projects }}
|
|
<option value="{{ $p }}">{{ $p }}</option>
|
|
{{ end }}
|
|
</select>
|
|
<input class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4"
|
|
type="text" id="label-value"
|
|
name="value" placeholder="Label" minlength="1" required>
|
|
<button type="submit" class="py-2 px-4 font-semibold rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{{ else }}
|
|
<div class="text-gray-300 text-sm mb-4 mt-6">You don't have any projects, yet. Start out by sending a few heartbeats before you can then assign labels.</div>
|
|
{{ end }}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full">
|
|
<hr class="border-t border-gray-800 my-4">
|
|
</div>
|
|
|
|
<!-- Language Mappings -->
|
|
<div class="w-full">
|
|
<div class="flex mb-8">
|
|
<div class="w-1/3 mr-4 inline-block">
|
|
<span class="font-semibold text-gray-300 text-lg">Language Mappings</span>
|
|
<p class="block text-sm text-gray-600">You can specify custom mapping from file extensions to programming languages, for instance a ".jsx" file could be mapped to the "React" language.</p>
|
|
</div>
|
|
|
|
<div class="w-2/3 ml-4 inline-block">
|
|
{{ if .LanguageMappings }}
|
|
<div class="mb-8">
|
|
<h3 class="inline-block font-semibold text-gray-300">Rules</h3>
|
|
{{ range $i, $mapping := .LanguageMappings }}
|
|
<div class="flex items-center">
|
|
<div class="text-gray-300 border-1 w-full inline-block my-1 py-1 text-align text-sm">
|
|
▸ When filename ends in <span
|
|
class="text-green-700 text-sm bg-gray-900 rounded py-1 px-2 font-semibold mr-1">{{ $mapping.Extension }}</span>
|
|
then change the <span class="font-semibold">language</span> to <span
|
|
class="text-green-700 text-sm bg-gray-900 rounded py-1 px-2 font-semibold mr-1">{{ $mapping.Language }}</span>
|
|
</div>
|
|
<form class="float-right" action="" method="post">
|
|
<input type="hidden" name="action" value="delete_mapping">
|
|
<input type="hidden" id="mapping_id" name="mapping_id" required value="{{ $mapping.ID }}">
|
|
<button type="submit" class="py-2 px-4 rounded bg-gray-850 hover:bg-gray-800 text-red-600 text-sm" title="Delete rule">✕</button>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<form action="" method="post">
|
|
<h3 class="inline-block font-semibold text-gray-300">Add Rule</h3>
|
|
|
|
<input type="hidden" name="action" value="add_mapping">
|
|
<div class="flex items-center w-full text-gray-500 text-sm">
|
|
<span class="mr-2">When filename ends in</span>
|
|
<input class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 cursor-pointer"
|
|
type="text" id="extension" style="width: 70px"
|
|
name="extension" placeholder=".py" minlength="1" required>
|
|
<span class="mx-2">change language to</span>
|
|
<input class="flex-grow appearance-none bg-gray-850 focus:bg-gray-800 text-gray-300 outline-none rounded py-2 px-4 cursor-pointer"
|
|
type="text" id="language" style="width: 100px"
|
|
name="language" placeholder="Python" minlength="1" required>
|
|
<div class="flex justify-end ml-4">
|
|
<button type="submit" class="py-2 px-4 font-semibold rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="permissions" class="tab flex flex-col space-y-4" v-if="isActive('permissions')">
|
|
<!-- TODO -->
|
|
<details class="mb-8 pb-8 border-b border-gray-700" id="details-public-data">
|
|
<summary class="cursor-pointer">
|
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
|
Public Data
|
|
</h2>
|
|
</summary>
|
|
|
|
<div>
|
|
<p class="text-gray-300 text-sm mb-4 mt-6">Some features require public access to your data without
|
|
authentication. This mainly includes <strong>Badges</strong> and the integration with <strong>GitHub
|
|
Readme Stats</strong>, corresponding to these API endpoints:</p>
|
|
<ul class="list-disc list-inside text-gray-300">
|
|
<li class="ml-2"><span class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono">/api/compat/shields/v1/{user}</span>
|
|
</li>
|
|
<li class="ml-2"><span class="text-white text-xs bg-gray-900 rounded py-1 px-2 font-mono">/api/v1/users/{user}/stats/{range}</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<form action="" method="post" class="mt-8">
|
|
<input type="hidden" name="action" value="update_sharing">
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<span class="mr-2">Publicly accessible data range:<br><span class="text-xs text-gray-500">(in days; 0 = not public, -1 = unlimited)</span></span>
|
|
<div>
|
|
<input class="shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3"
|
|
style="width: 70px;" type="number" id="max_days" name="max_days" min="-1" required
|
|
value="{{ .User.ShareDataMaxDays }}">
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share projects: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_projects"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareProjects }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareProjects }} selected {{ end }}>Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share languages: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_languages"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareLanguages }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareLanguages }} selected {{ end }}>Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share editors: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_editors"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareEditors }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareEditors }} selected {{ end }}>Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share operating systems: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_oss"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareOSs }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareOSs }} selected {{ end }}>
|
|
Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share machines: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_machines"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareMachines }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareMachines }} selected {{ end }}>Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center w-full text-gray-300 text-sm justify-between my-2">
|
|
<div class="flex justify-start">
|
|
<span class="mr-2">Share project labels: </span>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<select autocomplete="off" name="share_labels"
|
|
class="cursor-pointer shadow appearance-nonshadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded py-1 px-3">
|
|
<option value="false" class="cursor-pointer" {{ if not .User.ShareLabels }} selected {{ end }}>No
|
|
</option>
|
|
<option value="true" class="cursor-pointer" {{ if .User.ShareLabels }} selected {{ end }}>Yes
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-between float-right mt-4">
|
|
<button type="submit"
|
|
class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm"
|
|
style="width: 100px;">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div id="integrations" class="tab flex flex-col space-y-4" v-if="isActive('integrations')">
|
|
<!-- TODO -->
|
|
<details class="mb-8 pb-8 border-b border-gray-700" id="details-integrations">
|
|
<summary class="cursor-pointer">
|
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block"
|
|
id="integrations">
|
|
Integrations
|
|
</h2>
|
|
</summary>
|
|
<div class="w-full">
|
|
<div class="mt-8 text-gray-300 text-sm">
|
|
<h3 class="inline-block font-semibold text-md mb-4 border-b border-green-700">
|
|
WakaTime
|
|
</h3>
|
|
|
|
<div class="flex space-x-4">
|
|
<img alt="WakaTime Logo"
|
|
width="55px"
|
|
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzQwIiBoZWlnaHQ9IjM0MCIgdmlld0JveD0iMCAwIDM0MCAzNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTcwIDIwQzg3LjE1NiAyMCAyMCA4Ny4xNTYgMjAgMTcwQzIwIDI1Mi44NDQgODcuMTU2IDMyMCAxNzAgMzIwQzI1Mi44NDQgMzIwIDMyMCAyNTIuODQ0IDMyMCAxNzBDMzIwIDg3LjE1NiAyNTIuODQ0IDIwIDE3MCAyMFYyMFYyMFoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iNDAiLz4KPHBhdGggZD0iTTE5MC4xODMgMjEzLjU0MUMxODguNzQgMjE1LjQ0MyAxODYuNTc2IDIxNi42NjcgMTg0LjE1MSAyMTYuNjY3QzE4My45MTMgMjE2LjY2NyAxODMuNjc3IDIxNi42NTEgMTgzLjQ0MyAyMTYuNjI3QzE4My4wNDIgMjE2LjU3OSAxODIuODIzIDIxNi41NDUgMTgyLjYwNiAyMTYuNDk3QzE4Mi4zMzcgMjE2LjQzNCAxODIuMTM3IDIxNi4zNzUgMTgxLjk0IDIxNi4zMDhDMTgxLjU2MSAyMTYuMTc2IDE4MS4zOTIgMjE2LjEwOSAxODEuMjI4IDIxNi4wMzVDMTgwLjg0MyAyMTUuODQ5IDE4MC43MDcgMjE1Ljc3OCAxODAuNTcyIDIxNS43MDFDMTgwLjIwNSAyMTUuNDc4IDE4MC4xMDkgMjE1LjQxMiAxODAuMDE0IDIxNS4zNDVDMTc5Ljg1NiAyMTUuMjMzIDE3OS42OTggMjE1LjExNyAxNzkuNTQ3IDIxNC45OTJDMTc5LjI1MSAyMTQuNzQ2IDE3OS4xNDcgMjE0LjY1IDE3OS4wNDQgMjE0LjU1MkMxNzguNzMxIDIxNC4yNDEgMTc4LjUzMSAyMTQuMDE4IDE3OC4zNDEgMjEzLjc4NUMxNzcuOTgyIDIxMy4zMzEgMTc3LjY5IDIxMi44ODggMTc3LjQzOCAyMTIuNDE1TDE2OC42IDE5OC4yMTRMMTU5Ljc2NiAyMTIuNDE1QzE1OC4zOCAyMTQuOTM5IDE1NS44NzQgMjE2LjY2NyAxNTIuOTk1IDIxNi42NjdDMTUwLjEwNiAyMTYuNjY3IDE0Ny41ODggMjE0LjkyNiAxNDYuMjQzIDIxMi4zNDZMMTA3LjYwNyAxNTYuMDYxQzEwNi4zMzcgMTU0LjUyOSAxMDUuNTU2IDE1Mi40OTkgMTA1LjU1NiAxNTAuMjU4QzEwNS41NTYgMTQ1LjUxNCAxMDkuMDQzIDE0MS42NjUgMTEzLjM0NCAxNDEuNjY1QzExNi4xMjcgMTQxLjY2NSAxMTguNTY0IDE0My4yODIgMTE5Ljk0MiAxNDUuNzA4TDE1Mi41NTUgMTkzLjlMMTYxLjczNSAxNzguOTUyQzE2My4wNTggMTc2LjI4OCAxNjUuNjI2IDE3NC40NzggMTY4LjU3NSAxNzQuNDc4QzE3MS4yNzMgMTc0LjQ3OCAxNzMuNjUyIDE3NS45OTYgMTc1LjA0OSAxNzguMjk4TDE4NC41MTcgMTkzLjgzOUwyMzUuNjg0IDEyMC41ODNDMjM3LjA3NSAxMTguMjI2IDIzOS40NzUgMTE2LjY2NyAyNDIuMjEzIDExNi42NjdDMjQ2LjUxNCAxMTYuNjY3IDI1MCAxMjAuNTE0IDI1MCAxMjUuMjU4QzI1MCAxMjcuMzMyIDI0OS4zMzcgMTI5LjIzMiAyNDguMjMgMTMwLjcxNUwxOTAuMTgzIDIxMy41NDFWMjEzLjU0MVoiIGZpbGw9IndoaXRlIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEwIi8+Cjwvc3ZnPgo=">
|
|
<p class="text-sm">You can connect Wakapi with the official <a class="underline"
|
|
href="https://wakatime.com"
|
|
rel="noopener noreferrer"
|
|
target="_blank">WakaTime</a> in a
|
|
way
|
|
that all heartbeats sent to Wakapi are relayed. This way, you can use both services
|
|
at
|
|
the same time. To get started, <a class="underline"
|
|
href="https://wakatime.com/developers#authentication"
|
|
rel="noopener noreferrer"
|
|
target="_blank">get your API key</a> and paste it here.
|
|
</p>
|
|
</div>
|
|
|
|
<form action="" method="post">
|
|
<input type="hidden" name="action" value="toggle_wakatime">
|
|
|
|
{{ $placeholderText := "Paste your WakaTime API key here ..." }}
|
|
{{ if .User.WakatimeApiKey }}
|
|
{{ $placeholderText = "********" }}
|
|
{{ end }}
|
|
|
|
<div class="flex items-center mt-8 space-x-2">
|
|
<label class="text-gray-500 font-semibold">API Key:</label>
|
|
<input type="password" name="api_key" id="wakatime_api_key"
|
|
class="flex-grow shadow appearance-nonshadow appearance-none bg-gray-800 text-gray-300 border-green-700 border rounded py-1 px-3 {{ if not .User.WakatimeApiKey }}focus:bg-gray-700 focus:border-gray-500{{ end }}"
|
|
placeholder="{{ $placeholderText }}" {{ if .User.WakatimeApiKey }}readonly{{ end }}>
|
|
<div class="flex-grow flex justify-end">
|
|
{{ if not .User.WakatimeApiKey }}
|
|
<button type="submit"
|
|
class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
Connect
|
|
</button>
|
|
{{ else }}
|
|
<button type="submit"
|
|
class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm"
|
|
style="width: 130px">Disconnect
|
|
</button>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
{{ if .User.WakatimeApiKey }}
|
|
<div class="flex justify-end">
|
|
<button id="btn-import-wakatime" type="button" style="width: 130px"
|
|
class="py-1 px-3 my-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
|
<span class="iconify inline" data-icon="eva:corner-right-down-fill"></span> Import Data
|
|
</button>
|
|
</div>
|
|
{{ end }}
|
|
</form>
|
|
|
|
<form action="" method="post" id="form-import-wakatime" class="mt-6">
|
|
<input type="hidden" name="action" value="import_wakatime">
|
|
</form>
|
|
|
|
<p class="mt-6">
|
|
<span class="font-semibold"><span class="iconify inline"
|
|
data-icon="emojione-v1:backhand-index-pointing-right"></span> Please note:</span>
|
|
<span>When enabling this feature, the operators of this server will, in theory (!), have unlimited access to your data stored in WakaTime. If you are concerned about your privacy, please do not enable this integration or wait for OAuth 2 authentication (<a
|
|
class="underline" target="_blank" href="https://github.com/muety/wakapi/issues/94"
|
|
rel="noopener noreferrer">#94</a>) to be implemented.</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mt-10 text-gray-300 text-sm">
|
|
<h3 class="inline-block font-semibold text-md mb-4 border-b border-green-700">
|
|
Badges (Shields.io)
|
|
</h3>
|
|
|
|
{{ if ne .User.ShareDataMaxDays 0 }}
|
|
<div class="flex space-x-1">
|
|
<h3 class="font-semibold">Examples:</h3>
|
|
<span class="text-xs text-gray-500">(Only available on public instances, not on localhost)</span>
|
|
</div>
|
|
<div class="flex flex-col mb-4 mt-2">
|
|
<div class="flex justify-between my-2">
|
|
<div>
|
|
<img class="with-url-src"
|
|
src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today"
|
|
alt="Shields.io badge"/>
|
|
</div>
|
|
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto"
|
|
style="max-width: 300px;">
|
|
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:today&style=flat-square&color=blue&label=today
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between my-2">
|
|
<div>
|
|
<img class="with-url-src"
|
|
src="https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d"
|
|
alt="Shields.io badge"/>
|
|
</div>
|
|
<span class="with-url-inner text-xs bg-gray-900 rounded py-1 px-2 font-mono whitespace-no-wrap overflow-auto"
|
|
style="max-width: 300px;">
|
|
https://img.shields.io/endpoint?url=%s/api/compat/shields/v1/{{ .User.ID }}/interval:30_days&style=flat-square&color=blue&label=last 30d
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p>You can also add <span class="text-xs bg-gray-900 rounded py-1 px-2 font-mono">/project:your-cool-project</span>
|
|
to the URL to filter by project.</p>
|
|
{{ else }}
|
|
<p>You have the ability to create badges from your coding statistics using <a
|
|
href="https://shields.io" target="_blank" class="border-b border-green-800"
|
|
rel="noopener noreferrer">Shields.io</a>. To do so, you need to grant public, unauthorized
|
|
access to the respective endpoint. See <a href="settings#details-public-data" class="underline">Public
|
|
Data</a> setting.</p>
|
|
{{ end }}
|
|
</div>
|
|
|
|
<div class="mt-10 text-gray-300 text-sm">
|
|
<h3 class="inline-block font-semibold text-md mb-4 border-b border-green-700">
|
|
GitHub Readme Stats
|
|
</h3>
|
|
|
|
<p class="mb-4">Wakapi intregrates with <a
|
|
href="https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats"
|
|
class="underline" target="_blank" rel="noopener noreferrer">GitHub Readme Stats</a> to
|
|
generate fancy cards for you.</p>
|
|
|
|
{{ if ne .User.ShareDataMaxDays 0 }}
|
|
<div class="flex space-x-1">
|
|
<h3 class="font-semibold">Example:</h3>
|
|
<span class="text-xs text-gray-500">(Only available on public instances, not on localhost)</span>
|
|
</div>
|
|
<div class="flex flex-col mb-4 mt-2">
|
|
<img src="https://github-readme-stats.vercel.app/api/wakatime?username={{ .User.ID }}&api_domain=%s&bg_color=2D3748&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact"
|
|
class="with-url-src-no-scheme" alt="Readme Stats Card">
|
|
<p class="mt-2"><strong>Source URL:</strong>
|
|
<span class="break-words text-xs bg-gray-900 rounded py-1 px-2 font-mono with-url-inner-no-scheme">
|
|
https://github-readme-stats.vercel.app/api/wakatime?username={{ .User.ID }}&api_domain=%s&bg_color=2D3748&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact
|
|
</span>
|
|
</p>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div id="danger_zone" class="tab flex flex-col space-y-4" v-if="isActive('danger_zone')">
|
|
<!-- TODO -->
|
|
<details class="mb-8 pb-8" id="details-danger-zone">
|
|
<summary class="cursor-pointer">
|
|
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="danger">
|
|
<span class="iconify inline" data-icon="emojione-v1:warning"></span> Danger Zone
|
|
</h2>
|
|
</summary>
|
|
<div class="w-full">
|
|
<div class="mt-10 text-gray-300 text-sm">
|
|
<h3 class="font-semibold text-md mb-4 border-b border-green-700 inline-block">
|
|
Regenerate summaries
|
|
</h3>
|
|
<p>
|
|
Wakapi improves its efficiency and speed by automatically aggregating individual heartbeats to
|
|
summaries on a per-day basis.
|
|
That is, historic summaries, i.e. such from past days, are generated once and only fetched from
|
|
the
|
|
database in a static fashion afterwards, unless you pass <span
|
|
class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">&recompute=true</span>
|
|
with your request.
|
|
</p>
|
|
<p class="mt-2">
|
|
If, for some reason, these aggregated summaries are faulty or preconditions have change (e.g.
|
|
you
|
|
modified language mappings retrospectively), you may want to re-generate them from raw
|
|
heartbeats.
|
|
</p>
|
|
<p class="mt-2">
|
|
<strong>Note:</strong> Only run this action if you know what you are doing. Data might be lost
|
|
is
|
|
case heartbeats were deleted after the respective summaries had been generated.
|
|
</p>
|
|
</div>
|
|
<div class="mt-6 flex justify-center">
|
|
<form action="" method="post" id="form-regenerate-summaries">
|
|
<input type="hidden" name="action" value="regenerate_summaries">
|
|
<button type="button" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm"
|
|
id="btn-regenerate-summaries">
|
|
Clear & Regenerate
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-10 text-gray-300 text-sm">
|
|
<h3 class="font-semibold text-md mb-4 border-b border-green-700 inline-block" id="apikey">
|
|
Reset API Key
|
|
</h3>
|
|
|
|
<form class="mt-2" action="" method="post">
|
|
<input type="hidden" name="action" value="reset_apikey">
|
|
<div class="text-gray-300 text-sm mb-4">
|
|
<strong>⚠️ Caution:</strong> Resetting your API key requires you to update your <span
|
|
class="font-mono">.wakatime.cfg</span> files on all of your computers to make the
|
|
WakaTime
|
|
client send heartbeats again.
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<button type="submit"
|
|
class="mt-2 py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm">
|
|
Reset API Key
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-10 text-gray-300 text-sm">
|
|
<h3 class="font-semibold text-md mb-4 border-b border-green-700 inline-block">
|
|
Delete Account
|
|
</h3>
|
|
<p>
|
|
Deleting your account will cause all data, including all your heartbeats, to be erased from the
|
|
server immediately. This action is irreversible. <strong>Be careful!</strong>
|
|
</p>
|
|
</div>
|
|
<div class="mt-6 flex justify-center">
|
|
<form action="" method="post" id="form-delete-user">
|
|
<input type="hidden" name="action" value="delete_account">
|
|
<button type="button" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm"
|
|
id="btn-confirm-delete-user">
|
|
Delete my Account
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script type="text/javascript">
|
|
const btnRegenerate = document.querySelector('#btn-regenerate-summaries')
|
|
const formRegenerate = document.querySelector('#form-regenerate-summaries')
|
|
btnRegenerate.addEventListener('click', () => {
|
|
if (confirm('Are you sure?')) {
|
|
formRegenerate.submit()
|
|
}
|
|
})
|
|
|
|
const btnDelete = document.querySelector('#btn-confirm-delete-user')
|
|
const formDelete = document.querySelector('#form-delete-user')
|
|
btnDelete.addEventListener('click', () => {
|
|
if (confirm('Are you sure? This can not be undone!')) {
|
|
formDelete.submit()
|
|
}
|
|
})
|
|
|
|
const btnImportWakatime = document.querySelector('#btn-import-wakatime')
|
|
const formImportWakatime = document.querySelector('#form-import-wakatime')
|
|
if (btnImportWakatime) {
|
|
btnImportWakatime.addEventListener('click', () => {
|
|
if (confirm('Are you sure? The import can not be undone.')) {
|
|
formImportWakatime.submit()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Time zone stuff
|
|
|
|
const userTimeZone = {{ .User.Location }}
|
|
const userTzOffset = {{ .User.TZOffset.Hours }}
|
|
const selectTimezone = document.getElementById('select-timezone')
|
|
const createTzOption = (tz) => {
|
|
if (!tz) tz = 'Local'
|
|
const option = document.createElement('option')
|
|
option.setAttribute('value', tz)
|
|
option.innerText = tz
|
|
if (tz === userTimeZone) option.setAttribute('selected', 'true')
|
|
return option
|
|
}
|
|
|
|
const defaultOption = createTzOption('Local')
|
|
defaultOption.value = 'Local'
|
|
defaultOption.innerText = `Local server time (UTC+${userTzOffset})`
|
|
selectTimezone.appendChild(defaultOption)
|
|
|
|
tzs.sort()
|
|
.map(createTzOption)
|
|
.forEach(o => selectTimezone.appendChild(o))
|
|
|
|
const hash = location.hash.replace('#', '')
|
|
if (hash) {
|
|
const elem = document.getElementById(hash)
|
|
if (elem) elem.open = true
|
|
}
|
|
</script>
|
|
|
|
{{ template "footer.tpl.html" . }}
|
|
|
|
{{ template "foot.tpl.html" . }}
|
|
</body>
|
|
|
|
</html>
|
|
|