Spaces:
Running
Running
<template> | |
<div class="w-full px-3 shrink-0 flex flex-col justify-start items-center"> | |
<div | |
class="w-auto mx-auto mt-6 mb-2 flex flex-row justify-center items-center flex-wrap" | |
:class="state.latestBlog ?? 'invisible'" | |
> | |
<span | |
class="px-2 -mt-px leading-7 rounded mr-2 text-sm bg-green-100 text-green-600 font-medium" | |
>What's new</span | |
> | |
<!-- <a | |
class="text-gray-700 hover:underline" | |
:href="`/blog/${state.latestBlog?.slug}`" | |
> | |
{{ state.latestBlog?.title }} | |
<i class="fas fa-chevron-right mr-1 text-gray-500 text-sm"></i> | |
</a> --> | |
<a | |
class="text-gray-700 hover:underline" | |
href="https://www.bytebase.com/blog/bytebase-2-0/" | |
> | |
Bytebase 2.0 - the GitLab for Database DevOps | |
<i class="fas fa-chevron-right mr-1 text-gray-500 text-sm"></i> | |
</a> | |
</div> | |
<div | |
class="w-auto sm:w-full grow max-w-3xl 2xl:max-w-4xl mt-4 flex flex-row justify-center items-center shadow-inner border border-solid border-dark rounded" | |
> | |
<input | |
ref="inputElRef" | |
v-model="state.repo" | |
class="w-auto h-9 px-2 grow shrink text-dark outline-none rounded rounded-r-none placeholder:text-gray-300 focus:shadow-focus" | |
type="text" | |
:placeholder=" | |
state.repos.length > 0 | |
? '...add next repository' | |
: 'star-history or star-history/star-history or https://github.com/star-history/star-history' | |
" | |
@paste="handleInputerPasted" | |
@keydown="handleInputerKeyDown" | |
/> | |
<button | |
class="h-9 pl-4 pr-4 whitespace-nowrap w-auto text-dark border-l border-dark hover:bg-dark hover:text-light" | |
:class="isFetching ? 'cursor-wait' : ''" | |
@click="handleAddRepoBtnClick" | |
> | |
View star history | |
</button> | |
</div> | |
<!-- repo list --> | |
<div class="w-full mt-4 flex flex-row justify-center items-center"> | |
<div | |
v-if="state.repos.length > 0" | |
class="w-full max-w-2xl flex flex-row flex-wrap justify-center items-center" | |
> | |
<div | |
v-for="item of state.repos" | |
:key="item.name" | |
class="leading-8 px-3 pr-2 mb-2 text-dark rounded flex flex-row justify-center items-center border mr-3 last:mr-0" | |
> | |
<a :href="`https://github.com/${item.name}`" target="_blank"> | |
<i | |
class="fas fa-external-link-alt mr-2 fa-sm text-gray-400 hover:text-green-600" | |
></i> | |
</a> | |
<span | |
class="mr-1 cursor-pointer hover:line-through select-none" | |
:class="item.visible ? '' : 'line-through text-gray-400'" | |
@click="handleToggleRepoItemVisible(item.name)" | |
> | |
{{ item.name }} | |
</span> | |
<span | |
class="relative w-5 h-5 flex flex-row justify-center items-center cursor-pointer hover:opacity-60" | |
@click="handleDeleteRepoBtnClick(item.name)" | |
> | |
<span class="w-3 rotate-45 h-px bg-black absolute top-1/2"></span> | |
<span class="w-3 -rotate-45 h-px bg-black absolute top-1/2"></span> | |
</span> | |
</div> | |
<button | |
class="leading-8 mb-2 text-black hover:bg-gray-100 px-3 rounded border border-transparent" | |
@click="handleClearAllRepoBtnClick" | |
> | |
Clear all | |
</button> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script lang="ts" setup> | |
import { computed, onMounted, reactive, ref, watch } from "vue"; | |
import { head } from "lodash"; | |
import { GITHUB_REPO_URL_REG } from "../helpers/consts"; | |
import toast from "../helpers/toast"; | |
import useAppStore from "../store"; | |
interface State { | |
repo: string; | |
repos: { | |
name: string; | |
visible: boolean; | |
}[]; | |
latestBlog?: Blog; | |
} | |
const store = useAppStore(); | |
const state = reactive<State>({ | |
repo: "", | |
repos: [], | |
}); | |
const inputElRef = ref<HTMLInputElement | null>(null); | |
const isFetching = computed(() => { | |
return store.isFetching; | |
}); | |
onMounted(async () => { | |
const res = await fetch("/blog/data.json"); | |
const blogList = (await res.json()) as Blog[]; | |
for (const blog of blogList) { | |
if (blog.featured) { | |
state.latestBlog = blog; | |
break; | |
} | |
} | |
state.repos = store.repos.map((r) => { | |
return { | |
name: r, | |
visible: true, | |
}; | |
}); | |
}); | |
watch( | |
() => [store.repos, store.chartMode], | |
() => { | |
for (const r of state.repos) { | |
if (r.visible && !store.repos.includes(r.name)) { | |
state.repos.splice(state.repos.indexOf(r), 1); | |
} | |
} | |
let hash = ""; | |
if (store.repos.length > 0) { | |
hash = `#${store.repos.join("&")}&${store.chartMode}`; | |
} | |
// Sync location hash only right here | |
window.location.hash = hash; | |
} | |
); | |
const handleAddRepoBtnClick = () => { | |
if (store.isFetching) { | |
return; | |
} | |
let rawRepos = state.repo; | |
if (rawRepos === "" && store.repos.length === 0) { | |
rawRepos = "star-history/star-history"; | |
} | |
if (rawRepos === "") { | |
toast.warn("Please input the repo name"); | |
return; | |
} | |
for (const rawRepo of rawRepos.split(",")) { | |
let repo = ""; | |
// Match repo name from github repo links. e.g. https://github.com/star-history/star-history/issues -> star-history/star-history | |
if (GITHUB_REPO_URL_REG.test(rawRepo)) { | |
repo = (rawRepo.match(GITHUB_REPO_URL_REG) as string[])[1]; | |
} | |
repo = head(rawRepo.split("#")) as string; | |
if (repo === "") { | |
continue; | |
} | |
if (GITHUB_REPO_URL_REG.test(repo)) { | |
const regResult = GITHUB_REPO_URL_REG.exec(repo); | |
if (regResult && regResult[1]) { | |
repo = regResult[1]; | |
} | |
} | |
const valueList = repo.split("/"); | |
if (valueList.length === 1) { | |
// Auto-complete repo name. e.g. bytebase -> bytebase/bytebase | |
repo = `${valueList[0]}/${repo}`; | |
} else if (valueList.length >= 2) { | |
// Remove additional chars. e.g. bytebase/bytebase/123 -> bytebase/bytebase | |
repo = `${valueList[0]}/${valueList[1]}`; | |
} | |
for (const r of state.repos) { | |
if (r.name === repo) { | |
if (r.visible) { | |
toast.warn(`Repo ${repo} is already on the chart`); | |
} else { | |
r.visible = true; | |
store.setRepos( | |
state.repos.filter((r) => r.visible).map((r) => r.name) | |
); | |
} | |
state.repo = ""; | |
return; | |
} | |
} | |
state.repos.push({ | |
name: repo, | |
visible: true, | |
}); | |
store.addRepo(repo); | |
} | |
state.repo = ""; | |
}; | |
const handleToggleRepoItemVisible = (repo: string) => { | |
for (const r of state.repos) { | |
if (r.name === repo) { | |
r.visible = !r.visible; | |
break; | |
} | |
} | |
store.setRepos(state.repos.filter((r) => r.visible).map((r) => r.name)); | |
}; | |
const handleDeleteRepoBtnClick = (repo: string) => { | |
for (const r of state.repos) { | |
if (r.name === repo) { | |
state.repos.splice(state.repos.indexOf(r), 1); | |
break; | |
} | |
} | |
store.delRepo(repo); | |
}; | |
const handleClearAllRepoBtnClick = () => { | |
state.repos = []; | |
store.setRepos([]); | |
}; | |
const handleInputerPasted = async (event: ClipboardEvent) => { | |
if (!inputElRef.value) { | |
return; | |
} | |
const inputEl = inputElRef.value; | |
if (event.clipboardData) { | |
event.preventDefault(); | |
const text = event.clipboardData | |
.getData("text") | |
.replace(/(?:\r\n|\r|\n| )/g, ""); | |
const value = state.repo; | |
const prevStr = value.slice( | |
0, | |
Math.min(inputEl.selectionStart || 0, inputEl.selectionEnd || 0) | |
); | |
const nextStr = value.slice( | |
Math.max(inputEl.selectionStart || 0, inputEl.selectionEnd || 0) | |
); | |
state.repo = `${prevStr}${text}${nextStr}`; | |
} | |
}; | |
const handleInputerKeyDown = (event: KeyboardEvent) => { | |
if (event.key === "Enter") { | |
event.preventDefault(); | |
handleAddRepoBtnClick(); | |
} | |
}; | |
</script> | |
<style scoped> | |
input::placeholder { | |
color: #c2c2c2; | |
} | |
</style> | |