future #13

Merged
袁涛 merged 4 commits from future into main 2025-11-03 09:50:42 +08:00
9 changed files with 2313 additions and 1669 deletions
Showing only changes of commit 65a15341c9 - Show all commits

View File

@@ -88,6 +88,9 @@
--white-60: #ffffff99; /* 60% white */
--white-80: #ffffffcc; /* 80% white */
--white-90: #ffffffe6; /* 90% white */
--confirmFontSize: 0.8rem;
--confirmBg: rgba(0, 0, 0, 0.15);
}
body {

View File

@@ -27,7 +27,6 @@
"moment": "^2.30.1",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1"
},
"devDependencies": {

View File

@@ -13,19 +13,19 @@
</template>
<!-- 设置页面 -->
<transition
name="settings-slide"
v-show="isSettingsRoute"
appear>
<transition name="settings-slide" v-show="isSettingsRoute" appear>
<SettingsPage class="setting-page" />
</transition>
<Modal ref="modalRef" showInput />
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { ref, watch, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import '@/common/base.css'
import Modal from '@/components/Modal.vue'
// 导入页面组件
import NoteListPage from './pages/NoteListPage.vue'
@@ -33,6 +33,7 @@ import SettingsPage from './pages/SettingsPage.vue'
const route = useRoute()
const transitionName = ref('slide-left')
const modalRef = ref()
// 计算是否为设置页面路由
const isSettingsRoute = computed(() => {
@@ -53,15 +54,15 @@ watch(
transitionName.value = 'slide-right'
return
}
// 判断导航方向
const toDepth = toPath.split('/').length
const fromDepth = fromPath.split('/').length
// 如果是进入更深的页面(如从列表页进入编辑页),使用左滑动画
if (toDepth > fromDepth) {
transitionName.value = 'slide-left'
}
}
// 如果是返回上层页面(如从编辑页返回列表页),使用右滑动画
else if (toDepth < fromDepth) {
transitionName.value = 'slide-right'
@@ -72,8 +73,6 @@ watch(
}
}
)
// 无额外处理函数
</script>
<style lang="less" scoped>

View File

@@ -1,249 +0,0 @@
<template>
<div v-if="visible" class="pd-mask" @click="handleMaskClick">
<div class="pd-confirm" @click.stop>
<div class="pd-blur"></div>
<div class="pd-plan"></div>
<h2 class="pd-title" v-if="title">{{ title }}</h2>
<p class="pd-message">{{ message }}</p>
<div class="pd-buttons">
<button v-if="showConfirm" class="pd-button pd-confirm-btn" @click="handleConfirm">
{{ confirmText }}
</button>
<button v-if="showCancel" class="pd-button pd-cancel" @click="handleCancel">
{{ cancelText }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
const props = defineProps({
title: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
confirmText: {
type: String,
default: '确认',
},
cancelText: {
type: String,
default: '取消',
},
showConfirm: {
type: Boolean,
default: true,
},
showCancel: {
type: Boolean,
default: true,
},
maskClosable: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
const visible = defineModel('visible', { type: Boolean, default: false })
// Promise 控制变量
let resolvePromise, rejectPromise
// 返回 Promise 的方法,模拟原生 confirm 行为
const show = (options = {}) => {
// 更新 props 值
Object.assign(props, options)
// 显示对话框
visible.value = true
emit('update:visible', true)
// 返回 Promise
return new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
}
const handleConfirm = () => {
emit('confirm')
visible.value = false
emit('update:visible', false)
// 解决 Promise
if (resolvePromise) {
resolvePromise()
resolvePromise = null
rejectPromise = null
}
}
const handleCancel = () => {
emit('cancel')
visible.value = false
emit('update:visible', false)
// 拒绝 Promise
if (rejectPromise) {
rejectPromise()
resolvePromise = null
rejectPromise = null
}
}
const handleMaskClick = () => {
if (props.maskClosable) {
handleCancel() // 点击遮罩相当于取消
}
}
// 添加/移除 body 滚动锁定
const lockBodyScroll = () => {
document.body.style.overflow = 'hidden'
}
const unlockBodyScroll = () => {
document.body.style.overflow = ''
}
onMounted(() => {
if (visible.value) {
lockBodyScroll()
}
})
onUnmounted(() => {
unlockBodyScroll()
})
// 监听 visible 变化
watch(visible, newVal => {
if (newVal) {
lockBodyScroll()
} else {
unlockBodyScroll()
}
})
// 暴露 show 方法给父组件使用
defineExpose({
show,
})
</script>
<style scoped>
.pd-mask {
position: fixed;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999999999;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: inherit;
background: var(--confirmBg, inherit);
}
.pd-confirm {
background: inherit;
background: var(--confirmTheme, #fff);
text-align: center;
color: var(--confirmColor, #636363);
font-size: var(--confirmFontSize, 1rem);
max-width: 75vw;
min-width: 20em;
box-shadow: 0px 35px 35px -10px rgba(0, 0, 0, 0.33);
border-radius: 10px;
position: relative;
white-space: break-spaces;
word-break: break-all;
overflow: hidden;
}
.pd-blur,
.pd-plan {
position: absolute;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.pd-plan {
background: inherit;
background: rgba(255, 255, 255, 0.66);
filter: blur(10px) saturate(2);
}
.pd-title {
position: relative;
font-size: 1.3em;
min-height: 1em;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
margin: 0;
padding: 0.5em 0;
}
.pd-message {
position: relative;
border-top: 1px solid var(--confirmBtnBorder, #f1f1f1);
width: 100%;
max-height: 70vh;
border-bottom: 1px solid var(--confirmBtnBorder, #f1f1f1);
margin: 0 auto;
box-sizing: border-box;
padding: 2em 10%;
line-height: 1.4;
overflow: auto;
}
.pd-buttons {
display: flex;
}
.pd-button {
position: relative;
width: 100%;
font-size: 1em;
appearance: none;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
border: none;
border-right: 1px solid var(--confirmBtnBorder, #f1f1f1);
padding: 1em 0;
cursor: pointer;
outline: none;
}
/* 当同时显示确认和取消按钮时每个按钮占50%宽度 */
.pd-button:first-child:nth-last-child(2),
.pd-button:first-child:nth-last-child(2) ~ .pd-button {
width: 50%;
}
/* 当只显示一个按钮时该按钮占100%宽度 */
.pd-button:first-child:nth-last-child(1) {
width: 100%;
border-right: none;
}
/* 只有最后一个按钮没有右边框 */
.pd-button:last-child {
border-right: none;
}
</style>

View File

@@ -103,119 +103,79 @@
<script setup>
import { ref, computed, nextTick } from 'vue'
import { useAppStore } from '../stores/useAppStore'
import FolderItem from './FolderItem.vue'
const store = useAppStore()
const props = defineProps({
allCount: {
type: Number,
default: 0,
},
starredCount: {
type: Number,
default: 0,
},
trashCount: {
type: Number,
default: 0,
},
archiveCount: {
type: Number,
default: 0,
},
selectedFolder: {
type: String,
default: '',
},
lastSyncTime: {
type: String,
default: '10/10上午9:28',
},
onAllClick: {
type: Function,
default: null,
},
onStarredClick: {
type: Function,
default: null,
},
onTrashClick: {
type: Function,
default: null,
},
onArchiveClick: {
type: Function,
default: null,
},
onAddFolder: {
type: Function,
default: null,
},
onFolderClick: {
type: Function,
default: null,
},
})
// 添加文件夹相关状态
const showAddFolderModal = ref(false)
const newFolderName = ref('')
const folderInput = ref(null)
// 重命名文件夹相关状态
const showRenameFolderModal = ref(false)
const renameFolderId = ref(null)
const renameFolderName = ref('')
// 选择模式相关状态
const isSelectionMode = ref(false)
const selectedFolders = ref([])
const showDeleteConfirmModal = ref(false)
// 计算自定义文件夹(排除系统文件夹)
const customFolders = computed(() => {
return store.folders.filter(folder => !['all', 'starred', 'trash', 'archive'].includes(folder.id))
})
// 获取文件夹中的便签数量
const getFolderNoteCount = folderId => {
return store.notes.filter(note => note.folderId === folderId && !note.isDeleted).length
}
@@ -224,28 +184,21 @@ const getFolderNoteCount = folderId => {
const toggleSelectionMode = () => {
isSelectionMode.value = !isSelectionMode.value
// 退出选择模式时清空选中项
if (!isSelectionMode.value) {
selectedFolders.value = []
}
}
// 切换文件夹选中状态
const toggleFolderSelection = folderId => {
if (!isSelectionMode.value) return
const index = selectedFolders.value.indexOf(folderId)
if (index > -1) {
// 已选中,取消选中
selectedFolders.value.splice(index, 1)
} else {
// 未选中,添加选中
selectedFolders.value.push(folderId)
}
}
@@ -266,7 +219,6 @@ const handleFolderClick = folderId => {
const handleDeleteSelectedFolders = () => {
if (selectedFolders.value.length === 0) return
showDeleteConfirmModal.value = true
}
@@ -275,21 +227,15 @@ const handleDeleteSelectedFolders = () => {
const confirmDeleteSelectedFolders = async () => {
try {
// 删除选中的文件夹
for (const folderId of selectedFolders.value) {
// 跳过系统文件夹
if (['all', 'starred', 'trash', 'archive'].includes(folderId)) continue
await store.deleteFolder(folderId)
}
// 清空选中项并退出选择模式
selectedFolders.value = []
isSelectionMode.value = false
showDeleteConfirmModal.value = false
} catch (error) {
console.error('删除文件夹失败:', error)
@@ -300,11 +246,9 @@ const confirmDeleteSelectedFolders = async () => {
const handleDeleteFolder = folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
// 确认删除
if (confirm(`确定要删除文件夹 "${getFolderName(folderId)}" 吗?文件夹中的便签将移至"全部便签"。`)) {
try {
store.deleteFolder(folderId)
@@ -320,18 +264,13 @@ const handleEditFolder = folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
const folder = store.folders.find(f => f.id === folderId)
if (folder) {
renameFolderId.value = folderId
renameFolderName.value = folder.name
showRenameFolderModal.value = true
// 在下次DOM更新后聚焦输入框
nextTick(() => {
if (folderInput.value) {
folderInput.value.focus()
@@ -346,11 +285,8 @@ const confirmRenameFolder = async () => {
if (renameFolderName.value.trim() && renameFolderId.value) {
try {
await store.updateFolder(renameFolderId.value, { name: renameFolderName.value.trim() })
showRenameFolderModal.value = false
renameFolderId.value = null
renameFolderName.value = ''
} catch (error) {
console.error('重命名文件夹失败:', error)
@@ -362,7 +298,6 @@ const confirmRenameFolder = async () => {
const getFolderName = folderId => {
const folder = store.folders.find(f => f.id === folderId)
return folder ? folder.name : ''
}
@@ -396,27 +331,13 @@ const handleTrashClick = () => {
}
}
const handleArchiveClick = () => {
if (isSelectionMode.value) {
toggleFolderSelection('archive')
} else {
if (props.onArchiveClick) {
props.onArchiveClick()
}
}
}
const handleAddFolder = event => {
// 阻止事件冒泡到父元素
event.stopPropagation()
showAddFolderModal.value = true
newFolderName.value = ''
// 在下次DOM更新后聚焦输入框
nextTick(() => {
if (folderInput.value) {
folderInput.value.focus()
@@ -429,16 +350,12 @@ const confirmAddFolder = async () => {
try {
const newFolder = {
name: newFolderName.value.trim(),
id: `folder_${Date.now()}`, // 生成唯一ID
createdAt: new Date().toISOString(),
}
await store.addFolder(newFolder)
showAddFolderModal.value = false
newFolderName.value = ''
} catch (error) {
console.error('添加文件夹失败:', error)

View File

@@ -1,206 +1,297 @@
<template>
<div class="code-fun-flex-col code-fun-justify-start">
<div class="code-fun-flex-col code-fun-justify-start group_1">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-end group">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start code-fun-relative group_2">
<div class="code-fun-flex-col section_7">
<div class="code-fun-flex-col code-fun-justify-start section_1">
<div class="code-fun-flex-row section_11">
<div class="code-fun-flex-col code-fun-justify-start section_12" @click="handleCancel">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper_2">
<span class="text_4">{{ cancelText }}</span>
</div>
</div>
<div class="code-fun-flex-col code-fun-justify-start section_13 code-fun-ml-8" @click="handleConfirm">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper_3">
<span class="text_5">{{ confirmText }}</span>
</div>
</div>
</div>
</div>
<div class="code-fun-flex-col code-fun-justify-start code-fun-relative section_9">
<div class="code-fun-flex-col code-fun-justify-start section_10">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start text-wrapper">
<input
v-model="inputValue"
class="text_3"
:placeholder="placeholder"
@keyup.enter="handleConfirm"
/>
</div>
</div>
</div>
</div>
<div class="code-fun-flex-col section_8 pos">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start code-fun-self-stretch group_3">
<span class="text_2">{{ title }}</span>
</div>
<div class="code-fun-self-start divider"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
title: {
type: String,
default: '新建文件夹'
},
placeholder: {
type: String,
default: '新建文件夹'
},
confirmText: {
type: String,
default: '确定'
},
cancelText: {
type: String,
default: '取消'
}
});
const emit = defineEmits(['confirm', 'cancel']);
const inputValue = ref('');
const handleConfirm = () => {
emit('confirm', inputValue.value);
};
const handleCancel = () => {
emit('cancel');
};
</script>
<style scoped lang="less">
.group_1 {
padding-bottom: 1rem;
.group {
padding: 0.5rem 0 0.25rem;
.group_2 {
margin-right: 0.75rem;
width: 32.31rem;
.section_7 {
padding-top: 5.75rem;
background-color: #00000000;
width: 29.06rem;
height: 18.16rem;
background-image: url(https://codefun-proj-user-res-1256085488.cos.ap-guangzhou.myqcloud.com/686f20ecd54496f19f54e801/68e862ab9520a30011f388ff/17600601480475170758.png);
background-repeat: no-repeat;
background-size: 100% auto;
background-position: 0% 0%;
.section_1 {
margin-top: 7.88rem;
background-color: #00000000;
.section_11 {
padding: 0.5rem 0.5rem 0.75rem;
background-color: #f4f4f7;
border-radius: 0rem 0rem 0.75rem 0.75rem;
border: solid 0.032rem #edeee8;
.section_12 {
padding: 0.25rem 0;
background-color: #00000000;
width: 13.63rem;
height: 3.13rem;
.text-wrapper_2 {
margin: 0 0.25rem;
padding: 0.75rem 0;
background-color: #f2f2f2;
border-radius: 0.25rem;
width: 13.34rem;
border: solid 0.032rem #d7d7d7;
.text_4 {
color: #757575;
font-size: 1.23rem;
font-weight: 700;
line-height: 1.23rem;
}
}
}
.section_13 {
margin-right: 0.25rem;
padding-top: 0.25rem;
background-color: #00000000;
width: 13.59rem;
height: 3.06rem;
.text-wrapper_3 {
margin-left: 0.25rem;
padding: 0.75rem 0;
border-radius: 0.25rem;
background-image: url('assets/53062683132af1946e1a4953530af228.png');
background-size: 100% 100%;
background-repeat: no-repeat;
width: 13.34rem;
.text_5 {
color: #cddff2;
font-size: 1.19rem;
line-height: 1.19rem;
}
}
}
}
}
.section_9 {
margin-top: -12.5rem;
padding: 1.75rem 0 0.25rem;
background-color: #00000000;
.section_10 {
margin: 0 1.5rem;
padding-top: 0.25rem;
background-color: #00000000;
width: 25.88rem;
.text-wrapper {
margin: 0 0.25rem;
padding: 0.75rem 0;
background-color: #fefefe;
border-radius: 0.25rem;
width: 25.53rem;
border: solid 0.063rem #e1e1e1;
.text_3 {
margin-left: 1rem;
color: #d0d0d0;
font-size: 1.38rem;
font-weight: 700;
line-height: 1.38rem;
}
}
}
}
}
.section_8 {
padding: 0 0.13rem;
background-color: #00000000;
width: 32.31rem;
.group_3 {
padding: 1.5rem 0;
width: 32.06rem;
.text_2 {
margin-left: 10.5rem;
color: #7a7a7a;
font-size: 1.54rem;
font-weight: 700;
line-height: 1.54rem;
}
}
.divider {
background-color: #f0f0f0;
width: 29rem;
height: 0.094rem;
}
}
.pos {
position: absolute;
left: 0;
right: 0;
top: 0;
}
}
}
}
</style>
<template>
<div v-if="visible" class="pd-mask" @click="handleMaskClick">
<div class="pd-confirm" @click.stop>
<h2 class="pd-title">{{ title }}</h2>
<div class="pd-input-container" v-if="showInput">
<input v-model="inputModel" class="pd-input" :placeholder="inputPlaceholder" @keyup.enter="handleConfirm" />
</div>
<p class="pd-message" v-else>{{ message }}</p>
<div class="pd-plan"></div>
<div class="pd-buttons">
<button v-if="showConfirm" class="pd-button pd-confirm-btn" @click="handleConfirm">
{{ confirmText }}
</button>
<button v-if="showCancel" class="pd-button pd-cancel" @click="handleCancel">
{{ cancelText }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { watch, onMounted, onUnmounted } from 'vue'
const props = defineProps({
title: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
confirmText: {
type: String,
default: '确认',
},
cancelText: {
type: String,
default: '取消',
},
showConfirm: {
type: Boolean,
default: true,
},
showCancel: {
type: Boolean,
default: true,
},
maskClosable: {
type: Boolean,
default: false,
},
showInput: {
type: Boolean,
default: false,
},
inputPlaceholder: {
type: String,
default: '请输入文字',
},
inputValue: {
type: String,
default: '',
},
})
const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
const visible = defineModel('visible', { type: Boolean, default: false })
const inputModel = defineModel('inputValue', { type: String, default: '' })
// Promise 控制变量
let resolvePromise, rejectPromise
// 返回 Promise 的方法,模拟原生 confirm 行为
const show = (options = {}) => {
// 更新 props 值
Object.assign(props, options)
// 如果提供了 inputValue则更新输入框的值
if (options.inputValue !== undefined) {
inputModel.value = options.inputValue
}
// 显示对话框
visible.value = true
emit('update:visible', true)
// 返回 Promise
return new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
}
const handleConfirm = () => {
emit('confirm')
visible.value = false
emit('update:visible', false)
// 解决 Promise传递输入框的值
if (resolvePromise) {
resolvePromise(inputModel.value)
resolvePromise = null
rejectPromise = null
}
}
const handleCancel = () => {
emit('cancel')
visible.value = false
emit('update:visible', false)
// 拒绝 Promise
if (rejectPromise) {
rejectPromise()
resolvePromise = null
rejectPromise = null
}
}
const handleMaskClick = () => {
if (props.maskClosable) {
handleCancel() // 点击遮罩相当于取消
}
}
// 添加/移除 body 滚动锁定
const lockBodyScroll = () => {
document.body.style.overflow = 'hidden'
}
const unlockBodyScroll = () => {
document.body.style.overflow = ''
}
onMounted(() => {
if (visible.value) {
lockBodyScroll()
}
})
onUnmounted(() => {
unlockBodyScroll()
})
// 监听 visible 变化
watch(visible, newVal => {
if (newVal) {
lockBodyScroll()
} else {
unlockBodyScroll()
}
})
// 暴露 show 方法给父组件使用
defineExpose({ show })
</script>
<style scoped>
.pd-mask {
position: fixed;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999999999;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: inherit;
background: var(--confirmBg, inherit);
}
.pd-confirm {
background: inherit;
background: var(--confirmTheme, #fff);
text-align: center;
color: var(--confirmColor, #636363);
font-size: var(--confirmFontSize, 1rem);
max-width: 75vw;
min-width: 20em;
box-shadow: 0px 35px 35px -10px rgba(0, 0, 0, 0.33);
border-radius: 10px;
position: relative;
white-space: break-spaces;
word-break: break-all;
overflow: hidden;
}
.pd-plan {
background: inherit;
background: rgba(255, 255, 255, 0.66);
filter: blur(10px) saturate(2);
position: absolute;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.pd-title {
position: relative;
font-size: 1.3em;
min-height: 1em;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
margin: 0;
padding: 0.5em 0;
}
.pd-message {
position: relative;
border-top: 1px solid var(--confirmBtnBorder, #f1f1f1);
width: 100%;
max-height: 70vh;
border-bottom: 1px solid var(--confirmBtnBorder, #f1f1f1);
margin: 0 auto;
box-sizing: border-box;
padding: 2em 10%;
line-height: 1.4;
overflow: auto;
}
.pd-input-container {
margin-block: 1.5em;
padding: 0 10% 1em;
position: relative;
z-index: 2;
}
.pd-input {
width: 100%;
padding: 0.8em;
font-size: 1em;
border: 1px solid var(--confirmBtnBorder, #f1f1f1);
border-radius: 4px;
box-sizing: border-box;
outline: none;
background: var(--confirmTheme, #fff);
color: var(--confirmColor, #636363);
box-shadow: 0px 0px 4px inset rgba(0, 0, 0, 0.12);
}
.pd-input::placeholder {
color: var(--black-20);
}
.pd-input:focus {
border-color: #4d90fe;
}
.pd-input::selection {
background-color: #4d90fe; /* 选中时的背景颜色 */
color: #ffffff; /* 选中时的文字颜色 */
}
.pd-buttons {
display: flex;
}
.pd-button {
position: relative;
width: 100%;
font-size: 1em;
appearance: none;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
border: none;
border-right: 1px solid var(--confirmBtnBorder, #f1f1f1);
padding: 1em 0;
cursor: pointer;
outline: none;
}
/* 当同时显示确认和取消按钮时每个按钮占50%宽度 */
.pd-button:first-child:nth-last-child(2),
.pd-button:first-child:nth-last-child(2) ~ .pd-button {
width: 50%;
}
/* 当只显示一个按钮时该按钮占100%宽度 */
.pd-button:first-child:nth-last-child(1) {
width: 100%;
border-right: none;
}
/* 只有最后一个按钮没有右边框 */
.pd-button:last-child {
border-right: none;
}
</style>

View File

@@ -283,7 +283,7 @@ const handleTrashNotesClick = () => {
setIsFolderExpanded(false)
}
const handleFolderClick = (folderId) => {
const handleFolderClick = folderId => {
setCurrentFolder(folderId)
setIsFolderExpanded(false)
}