Skip to content

Commit

Permalink
feat: added a profile settings and settings nav (#432)
Browse files Browse the repository at this point in the history
  • Loading branch information
LittleSound authored Dec 26, 2022
1 parent c8a7e6e commit 613c531
Show file tree
Hide file tree
Showing 23 changed files with 698 additions and 7 deletions.
11 changes: 11 additions & 0 deletions components/account/AccountHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ watchEffect(() => {
namedFields.value = named
iconFields.value = icons
})
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
</script>

<template>
Expand Down Expand Up @@ -91,6 +93,15 @@ watchEffect(() => {
<div absolute top-18 right-0 flex gap-2 items-center>
<AccountMoreButton :account="account" :command="command" />
<AccountFollowButton :account="account" :command="command" />
<!-- Edit profile -->
<NuxtLink
v-if="isSelf"
to="/settings/profile/appearance"
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 w-30 h-fit py1
hover="border-primary text-primary bg-active"
>
{{ $t('settings.profile.appearance.title') }}
</NuxtLink>
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
<div rounded p2 group-hover="bg-rose/10">
<div i-ri:bell-line />
Expand Down
109 changes: 109 additions & 0 deletions components/common/CommonCropImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script lang="ts" setup>
import type { Boundaries } from 'vue-advanced-cropper'
import { Cropper } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
export interface Props {
/** Images to be cropped */
modelValue?: File
/** Crop frame aspect ratio (width/height), default 1/1 */
stencilAspectRatio?: number
/** The ratio of the longest edge of the cut box to the length of the cut screen, default 0.9, not more than 1 */
stencilSizePercentage?: number
}
const props = withDefaults(defineProps<Props>(), {
stencilAspectRatio: 1 / 1,
stencilSizePercentage: 0.9,
})
const emits = defineEmits<{
(event: 'update:modelValue', value: File): void
}>()
const vmFile = useVModel(props, 'modelValue', emits, { passive: true })
const cropperDialog = ref(false)
const cropper = ref<InstanceType<typeof Cropper>>()
const cropperFlag = ref(false)
const cropperImage = reactive({
src: '',
type: 'image/jpg',
})
const stencilSize = ({ boundaries }: { boundaries: Boundaries }) => {
return {
width: boundaries.width * props.stencilSizePercentage,
height: boundaries.height * props.stencilSizePercentage,
}
}
watch(vmFile, (file, _, onCleanup) => {
let expired = false
onCleanup(() => expired = true)
if (file && !cropperFlag.value) {
cropperDialog.value = true
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (e) => {
if (expired)
return
cropperImage.src = e.target?.result as string
cropperImage.type = file.type
}
}
cropperFlag.value = false
})
const cropImage = () => {
if (cropper.value && vmFile.value) {
cropperFlag.value = true
cropperDialog.value = false
const { canvas } = cropper.value.getResult()
canvas?.toBlob((blob) => {
vmFile.value = new File([blob as any], `cropped${vmFile.value?.name}` as string, { type: blob?.type })
}, cropperImage.type)
}
}
</script>

<template>
<ModalDialog v-model="cropperDialog" :use-v-if="false" max-w-500px flex>
<div flex-1 w-0>
<div text-lg text-center my2 px3>
<h1>
{{ $t('action.edit') }}
</h1>
</div>
<div aspect-ratio-1>
<Cropper
ref="cropper"
class="overflow-hidden w-full h-full"
:src="cropperImage.src"
:resize-image="{
adjustStencil: false,
}"
:stencil-size="stencilSize"
:stencil-props="{
aspectRatio: props.stencilAspectRatio,
movable: false,
resizable: false,
handlers: {},
}"
image-restriction="stencil"
/>
</div>
<div m-4>
<button
btn-solid w-full rounded text-sm
@click="cropImage()"
>
{{ $t('action.confirm') }}
</button>
</div>
</div>
</ModalDialog>
</template>
121 changes: 121 additions & 0 deletions components/common/CommonInputImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{
modelValue?: File
/** The image src before change */
original?: string
/** Allowed file types */
allowedFileTypes?: string[]
/** Allowed file size */
allowedFileSize?: number
imgClass?: string
loading?: boolean
}>(), {
allowedFileTypes: () => ['image/jpeg', 'image/png'],
allowedFileSize: 1024 * 1024 * 5, // 5 MB
})
const emits = defineEmits<{
(event: 'update:modelValue', value: File): void
(event: 'error', code: number, message: string): void
}>()
const vmFile = useVModel(props, 'modelValue', emits, { passive: true })
const { t } = useI18n()
const elInput = ref<HTMLInputElement>()
function clearInput() {
if (elInput.value)
elInput.value.value = ''
}
function selectImage(e: Event) {
const target = e.target as HTMLInputElement
const image = target.files?.[0]
if (!image) {
vmFile.value = image
}
else if (!props.allowedFileTypes.includes(image.type)) {
emits('error', 1, t('error.unsupported_file_format'))
clearInput()
}
else if (image.size > props.allowedFileSize) {
emits('error', 2, t('error.file_size_cannot_exceed_n_mb', [5]))
clearInput()
}
else {
vmFile.value = image
clearInput()
}
}
const defaultImage = computed(() => props.original || '')
/** Preview of selected images */
const previewImage = ref('')
/** The current images on display */
const imageSrc = computed<string>(() => previewImage.value || defaultImage.value)
// Update the preview image when the input file change
watch(vmFile, (image, _, onCleanup) => {
let expired = false
onCleanup(() => expired = true)
if (image) {
const reader = new FileReader()
reader.readAsDataURL(image)
reader.onload = (e) => {
if (expired)
return
previewImage.value = e.target?.result as string
}
}
else {
previewImage.value = ''
clearInput()
}
})
defineExpose({
clearInput,
})
</script>

<template>
<label
class="bg-slate-500/10 focus-within:(outline outline-primary)"
relative
flex justify-center items-center
cursor-pointer
of-hidden
>
<img
v-if="imageSrc"
:src="imageSrc"
:class="imgClass || ''"
object-cover
w-full
h-full
>
<div absolute bg="black/50" text-white rounded-full text-xl w12 h12 flex justify-center items-center hover="bg-black/40 text-primary">
<div i-ri:upload-line />
</div>

<div
v-if="loading"
absolute inset-0
bg="black/30" text-white
flex justify-center items-center
>
<div class="i-ri:loader-4-line animate-spin animate-duration-[2.5s]" text-4xl />
</div>
<input
ref="elInput"
type="file"
absolute opacity-0 inset-0 z--1
:accept="allowedFileTypes.join(',')"
@change="selectImage"
>
</label>
</template>
1 change: 1 addition & 0 deletions components/nav/NavSide.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ const { notifications } = useNotifications()
<NavSideItem :text="$t('nav_side.conversations')" to="/conversations" icon="i-ri:at-line" user-only />
<NavSideItem :text="$t('nav_side.favourites')" to="/favourites" icon="i-ri:heart-3-line" user-only />
<NavSideItem :text="$t('nav_side.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line " user-only />
<NavSideItem :text="$t('nav_side.settings')" to="/settings" icon="i-ri:settings-4-line " user-only />
</nav>
</template>
60 changes: 60 additions & 0 deletions components/settings/SettingsNavItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts" setup>
const props = defineProps<{
text?: string
icon?: string
to: string | Record<string, string>
command?: boolean
}>()
const router = useRouter()
if (props.command) {
useCommand({
scope: 'Settings',
name: () => props.text ?? (typeof props.to === 'string' ? props.to as string : props.to.name),
icon: () => props.icon || '',
onActivate() {
router.push(props.to)
},
})
}
</script>

<template>
<NuxtLink
:to="to"
exact-active-class="text-primary"
block w-full group focus:outline-none
@click="$scrollToTop"
>
<div
w-full flex w-fit px5 py3 md:gap2 gap4 items-center
transition-250 group-hover:bg-active
group-focus-visible:ring="2 current"
>
<div flex-1 flex items-center md:gap2 gap4>
<div
flex items-center justify-center
:class="$slots.description ? 'w-12 h-12' : ''"
>
<slot name="icon">
<div v-if="icon" :class="icon" md:text-size-inherit text-xl />
</slot>
</div>
<div space-y-1>
<p>
<slot>
<span>{{ text }}</span>
</slot>
</p>
<p v-if="$slots.description" text-sm text-secondary>
<slot name="description" />
</p>
</div>
</div>
<div i-ri:arrow-right-s-line text-xl text-secondary-light />
</div>
</NuxtLink>
</template>
7 changes: 7 additions & 0 deletions components/user/UserSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ const switchUser = (user: UserLogin) => {
icon="i-ri:user-add-line"
@click="openSigninDialog"
/>

<NuxtLink to="/settings">
<CommonDropdownItem
:text="$t('nav_side.settings')"
icon="i-ri:settings-4-line"
/>
</NuxtLink>
<CommonDropdownItem
v-if="isMastoInitialised && currentUser"
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
Expand Down
1 change: 1 addition & 0 deletions composables/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const scopes = [
'Account',
'Languages',
'Switch account',
'Settings',
] as const

export type CommandScopeNames = typeof scopes[number]
Expand Down
18 changes: 18 additions & 0 deletions composables/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: AccountCr
return masto
}

export function setAccountInfo(userId: string, account: AccountCredentials) {
const index = getUsersIndexByUserId(userId)
if (index === -1)
return false

users.value[index].account = account
return true
}

export async function pullMyAccountInfo() {
const me = await useMasto().accounts.verifyCredentials()
setAccountInfo(currentUserId.value, me)
}

export function getUsersIndexByUserId(userId: string) {
return users.value.findIndex(u => u.account?.id === userId)
}

export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) {
// clear push subscription
user.pushSubscription = undefined
Expand Down
2 changes: 2 additions & 0 deletions composables/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const useIsMac = () => computed(() =>
useRequestHeaders(['user-agent'])['user-agent']?.includes('Macintosh')
?? navigator?.platform?.includes('Mac') ?? false)

export const isEmptyObject = (object: Object) => Object.keys(object).length === 0

export function removeHTMLTags(str: string) {
return str.replaceAll(HTMLTagRE, '')
}
1 change: 0 additions & 1 deletion https-dev-config/local-https-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ async function run() {
}

run()

Loading

0 comments on commit 613c531

Please sign in to comment.