Skip to content

Commit

Permalink
Navbar for community pages (#958)
Browse files Browse the repository at this point in the history
* navbar for community

* comment for NavbarWrapper centered

* adjust responsive breakpoint
  • Loading branch information
nighca authored Sep 29, 2024
1 parent 2aac9aa commit e17c142
Show file tree
Hide file tree
Showing 34 changed files with 648 additions and 14 deletions.
4 changes: 2 additions & 2 deletions spx-gui/src/components/community/CenteredWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
</template>

<style scoped lang="scss">
$threshold: 1920px; // TODO: reconfirm here with @qingqing
@import '@/components/ui/responsive';
.centered {
position: relative;
margin-left: auto;
margin-right: auto;
width: 1020px;
@media (min-width: $threshold) {
@include responsive(desktop-large) {
width: 1280px;
}
}
Expand Down
50 changes: 50 additions & 0 deletions spx-gui/src/components/community/CommunityNavbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<NavbarWrapper centered>
<template #left>
<NavbarDropdown>
<template #trigger> Create(TODO) </template>
<UIMenu>
<NavbarNewProjectItem />
<NavbarOpenProjectItem />
</UIMenu>
</NavbarDropdown>
</template>
<template #right>
<div class="search">
<UITextInput
v-model:value="searchInput"
:placeholder="$t({ en: 'Search project', zh: '搜索项目' })"
@keypress.enter="handleSearch"
>
<template #prefix><UIIcon class="search-icon" type="search" /></template>
</UITextInput>
</div>
</template>
</NavbarWrapper>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { UIMenu, UITextInput, UIIcon } from '@/components/ui'
import NavbarWrapper from '@/components/navbar/NavbarWrapper.vue'
import NavbarDropdown from '../navbar/NavbarDropdown.vue'
import NavbarNewProjectItem from '@/components/navbar/NavbarNewProjectItem.vue'
import NavbarOpenProjectItem from '@/components/navbar/NavbarOpenProjectItem.vue'
import { getProjectsRoute } from '@/router'
const router = useRouter()
const searchInput = ref('')
function handleSearch() {
router.push(getProjectsRoute(searchInput.value))
}
</script>

<style lang="scss" scoped>
.search {
width: 340px;
display: flex;
align-items: center;
}
</style>
8 changes: 0 additions & 8 deletions spx-gui/src/components/community/TopNav.vue

This file was deleted.

276 changes: 276 additions & 0 deletions spx-gui/src/components/editor/navbar/EditorNavbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<NavbarWrapper>
<template #left>
<NavbarDropdown>
<template #trigger>
<UIIcon type="file" />
</template>
<UIMenu>
<UIMenuGroup :disabled="!isOnline">
<NavbarNewProjectItem />
<NavbarOpenProjectItem />
</UIMenuGroup>
<UIMenuGroup :disabled="project == null">
<UIMenuItem @click="handleImportProjectFile">
<template #icon><img :src="importProjectSvg" /></template>
{{ $t({ en: 'Import project file...', zh: '导入项目文件...' }) }}
</UIMenuItem>
<UIMenuItem @click="handleExportProjectFile">
<template #icon><img :src="exportProjectSvg" /></template>
{{ $t({ en: 'Export project file', zh: '导出项目文件' }) }}
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup :disabled="project == null">
<UIMenuItem @click="handleImportFromScratch">
<template #icon><img :src="importScratchSvg" /></template>
{{ $t({ en: 'Import assets from Scratch file', zh: '从 Scratch 项目文件导入' }) }}
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup :disabled="project == null || !isOnline">
<UIMenuItem @click="handleShareProject">
<template #icon><img :src="shareSvg" /></template>
{{ $t({ en: 'Share project', zh: '分享项目' }) }}
</UIMenuItem>
<UIMenuItem
v-if="project?.isPublic === IsPublic.public"
@click="handleStopSharingProject"
>
<template #icon><img :src="stopSharingSvg" /></template>
{{ $t({ en: 'Stop sharing', zh: '停止分享' }) }}
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup :disabled="project == null">
<UIMenuItem @click="handleRemoveProject">
<template #icon><img :src="removeProjectSvg" /></template>
{{ $t({ en: 'Remove project', zh: '删除项目' }) }}
</UIMenuItem>
</UIMenuGroup>
</UIMenu>
</NavbarDropdown>
<NavbarDropdown v-if="project != null">
<template #trigger>
<UIIcon type="clock" />
</template>
<UIMenu>
<UIMenuItem :disabled="undoAction == null" @click="handleUndo.fn">
<template #icon><img :src="undoSvg" /></template>
<span class="history-menu-text">{{ $t(undoText) }}</span>
</UIMenuItem>
<UIMenuItem :disabled="redoAction == null" @click="handleRedo.fn">
<template #icon><img :src="redoSvg" /></template>
<span class="history-menu-text">{{ $t(redoText) }}</span>
</UIMenuItem>
</UIMenu>
</NavbarDropdown>
</template>
<template #center>
<template v-if="project != null">
<div class="project-name">{{ project.name }}</div>
<div class="auto-save-state">
<UITooltip placement="right">
<template #trigger>
<div
:class="['icon', autoSaveStateIcon.stateClass]"
v-html="autoSaveStateIcon.svg"
></div>
</template>
{{ $t(autoSaveStateIcon.desc) }}
</UITooltip>
</div>
</template>
</template>
</NavbarWrapper>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import saveAs from 'file-saver'
import {
UIMenu,
UIMenuGroup,
UIMenuItem,
UIIcon,
UITooltip,
useConfirmDialog
} from '@/components/ui'
import { useMessageHandle } from '@/utils/exception'
import { useI18n, type LocaleMessage } from '@/utils/i18n'
import { useNetwork } from '@/utils/network'
import { selectFile } from '@/utils/file'
import { AutoSaveToCloudState, type Project } from '@/models/project'
import { IsPublic } from '@/apis/common'
import { useRemoveProject, useShareProject, useStopSharingProject } from '@/components/project'
import { useLoadFromScratchModal } from '@/components/asset'
import NavbarWrapper from '@/components/navbar/NavbarWrapper.vue'
import NavbarDropdown from '@/components/navbar/NavbarDropdown.vue'
import NavbarNewProjectItem from '@/components/navbar/NavbarNewProjectItem.vue'
import NavbarOpenProjectItem from '@/components/navbar/NavbarOpenProjectItem.vue'
import undoSvg from './icons/undo.svg'
import redoSvg from './icons/redo.svg'
import importProjectSvg from './icons/import-project.svg'
import exportProjectSvg from './icons/export-project.svg'
import removeProjectSvg from './icons/remove-project.svg'
import importScratchSvg from './icons/import-scratch.svg'
import shareSvg from './icons/share.svg'
import stopSharingSvg from './icons/stop-sharing.svg'
import offlineSvg from './icons/offline.svg?raw'
import savingSvg from './icons/saving.svg?raw'
import failedToSaveSvg from './icons/failed-to-save.svg?raw'
import cloudCheckSvg from './icons/cloud-check.svg?raw'
const props = defineProps<{
project: Project | null
}>()
const { isOnline } = useNetwork()
const i18n = useI18n()
const router = useRouter()
const confirm = useConfirmDialog()
const importProjectFileMessage = { en: 'Import project file', zh: '导入项目文件' }
const handleImportProjectFile = useMessageHandle(
async () => {
await confirm({
title: i18n.t(importProjectFileMessage),
content: i18n.t({
en: 'Existing content of current project will be replaced with imported content. Are you sure to continue?',
zh: '当前项目中的内容将被导入项目文件的内容覆盖,确定继续吗?'
}),
confirmText: i18n.t({ en: 'Continue', zh: '继续' })
})
const file = await selectFile({ accept: '.gbp' })
const action = { name: importProjectFileMessage }
await props.project?.history.doAction(action, () => props.project!.loadGbpFile(file))
},
{ en: 'Failed to import project file', zh: '导入项目文件失败' }
).fn
const handleExportProjectFile = useMessageHandle(
async () => {
const gbpFile = await props.project!.exportGbpFile()
saveAs(gbpFile, gbpFile.name) // TODO: what if user cancelled download?
},
{ en: 'Failed to export project file', zh: '导出项目文件失败' }
).fn
const loadFromScratchModal = useLoadFromScratchModal()
const handleImportFromScratch = useMessageHandle(() => loadFromScratchModal(props.project!), {
en: 'Failed to import from Scratch file',
zh: '从 Scratch 项目文件导入失败'
}).fn
const shareProject = useShareProject()
const handleShareProject = useMessageHandle(() => shareProject(props.project!), {
en: 'Failed to share project',
zh: '分享项目失败'
}).fn
const stopSharingProject = useStopSharingProject()
const handleStopSharingProject = useMessageHandle(
() => stopSharingProject(props.project!),
{ en: 'Failed to stop sharing project', zh: '停止分享项目失败' },
{ en: 'Project sharing is now stopped', zh: '项目已停止分享' }
).fn
const removeProject = useRemoveProject()
const handleRemoveProject = useMessageHandle(
async () => {
await removeProject(props.project!.owner!, props.project!.name!)
router.push('/')
},
{ en: 'Failed to remove project', zh: '删除项目失败' }
).fn
const undoAction = computed(() => props.project?.history.getUndoAction() ?? null)
const undoText = computed(() => ({
en: undoAction.value != null ? `Undo "${undoAction.value.name.en}"` : 'Undo',
zh: undoAction.value != null ? `撤销“${undoAction.value.name.zh}”` : '撤销'
}))
const redoAction = computed(() => props.project?.history.getRedoAction() ?? null)
const redoText = computed(() => ({
en: redoAction.value != null ? `Redo "${redoAction.value.name.en}"` : 'Redo',
zh: redoAction.value != null ? `重做“${redoAction.value.name.zh}”` : '重做'
}))
const handleUndo = useMessageHandle(() => props.project!.history.undo(), {
en: 'Failed to undo',
zh: '撤销操作失败'
})
const handleRedo = useMessageHandle(() => props.project!.history.redo(), {
en: 'Failed to redo',
zh: '重做操作失败'
})
type AutoSaveStateIcon = {
svg: string
stateClass?: string
desc: LocaleMessage
}
const autoSaveStateIcon = computed<AutoSaveStateIcon>(() => {
if (!isOnline.value)
return { svg: offlineSvg, desc: { en: 'No internet connection', zh: '无网络连接' } }
switch (props.project?.autoSaveToCloudState) {
case AutoSaveToCloudState.Saved:
return { svg: cloudCheckSvg, desc: { en: 'Saved', zh: '已保存' } }
case AutoSaveToCloudState.Pending:
return {
svg: savingSvg,
stateClass: 'pending',
desc: { en: 'Pending save', zh: '待保存' }
}
case AutoSaveToCloudState.Saving:
return { svg: savingSvg, stateClass: 'saving', desc: { en: 'Saving', zh: '保存中' } }
case AutoSaveToCloudState.Failed:
return { svg: failedToSaveSvg, desc: { en: 'Failed to save', zh: '保存失败' } }
default:
throw new Error('unknown auto save state')
}
})
</script>
<style lang="scss" scoped>
.project-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 16px;
}
.auto-save-state {
margin-left: 8px;
cursor: pointer;
.icon {
width: 24px;
height: 24px;
:deep(svg) {
width: 100%;
height: 100%;
}
&.pending :deep(svg) path,
&.saving :deep(svg) path {
stroke-dasharray: 2;
}
&.saving :deep(svg) path {
animation: dash 1s linear infinite;
@keyframes dash {
to {
stroke-dashoffset: 24;
}
}
}
}
}
</style>
38 changes: 38 additions & 0 deletions spx-gui/src/components/navbar/NavbarDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<UIDropdown placement="bottom-start">
<template #trigger>
<div class="dropdown">
<slot name="trigger"></slot>
<UIIcon class="icon-arrow" type="arrowDown" />
</div>
</template>
<slot></slot>
</UIDropdown>
</template>

<script setup lang="ts">
import { UIDropdown, UIIcon } from '@/components/ui'
</script>

<style lang="scss" scoped>
.dropdown {
// TODO: common style with NavbarLang
height: 100%;
padding: 0 20px;
display: flex;
align-items: center;
&:hover {
background-color: var(--ui-color-primary-600);
}
:deep(.ui-icon) {
width: 24px;
height: 24px;
}
.icon-arrow {
width: 16px;
height: 16px;
}
}
</style>
Loading

0 comments on commit e17c142

Please sign in to comment.