Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/AdminDashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ watch(currentPage, () => {
if (feedbackContainer.value) {
feedbackContainer.value.scrollTop = 0
}
})
}, { flush: 'post' })
</script>

<template>
Expand Down
1 change: 1 addition & 0 deletions app/components/chat/ChatPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const suggestions = [
<button
v-for="suggestion in suggestions"
:key="suggestion.title"
type="button"
class="flex sm:flex-col gap-3 p-4 rounded-lg border border-default bg-default hover:bg-elevated/50 text-left transition-colors cursor-pointer"
@click="askQuestion(suggestion.question)"
>
Expand Down
2 changes: 1 addition & 1 deletion app/composables/useCanonical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function useCanonical(markdownAlternate?: MaybeRefOrGetter<string | null
const route = useRoute()
const site = useSiteConfig()

useHead({
useHeadSafe({
link: computed(() => {
const links: Link[] = [
{ rel: 'canonical', href: joinURL(site.url, route.path) }
Expand Down
25 changes: 19 additions & 6 deletions app/composables/useDateRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ export interface DateRange {
end: Date
}

interface SerializedDateRange {
start: string
end: string
}

export function useDateRange() {
const dateRange = useState<DateRange>('feedback-date-range', () => ({
start: subDays(new Date(), 30),
end: new Date()
const serializedDateRange = useState<SerializedDateRange>('feedback-date-range', () => {
const end = endOfDay(new Date())
return {
start: startOfDay(subDays(end, 30)).toISOString(),
end: end.toISOString()
}
})

const dateRange = computed<DateRange>(() => ({
start: new Date(serializedDateRange.value.start),
end: new Date(serializedDateRange.value.end)
}))

const setDateRange = (range: DateRange) => {
dateRange.value = {
start: startOfDay(range.start),
end: endOfDay(range.end)
serializedDateRange.value = {
start: startOfDay(range.start).toISOString(),
end: endOfDay(range.end).toISOString()
}
}

Expand Down
8 changes: 3 additions & 5 deletions app/composables/useSponsors.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { Sponsor } from '#shared/types'

export const useSponsors = async () => {
const [{ data: apiSponsors }, { data: manualSponsors }] = await Promise.all([
useFetch('/api/sponsors', { key: 'sponsors' }),
useAsyncData('manual-sponsors', () => queryCollection('manualSponsors').first())
])
export const useSponsors = () => {
const { data: apiSponsors } = useFetch('/api/sponsors', { key: 'sponsors' })
const { data: manualSponsors } = useAsyncData('manual-sponsors', () => queryCollection('manualSponsors').first())

const sponsors = computed(() => {
const manual = manualSponsors.value?.sponsors || []
Expand Down
9 changes: 8 additions & 1 deletion app/pages/deploy/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ await fetchList()
}"
>
<template #leading>
<NuxtImg v-if="deployment.logoSrc" :src="deployment.logoSrc" width="40" height="40" class="w-10 h-10" />
<NuxtImg
v-if="deployment.logoSrc"
:src="deployment.logoSrc"
:alt="`${deployment.title} logo`"
width="40"
height="40"
class="w-10 h-10"
/>
<UIcon v-else :name="deployment.logoIcon" class="size-10 text-black dark:text-white" />
</template>
<UBadge
Expand Down
2 changes: 1 addition & 1 deletion app/pages/modules/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ watch(scrollY, (y) => {
if (window.innerHeight + y >= document.documentElement.scrollHeight - SCROLL_THRESHOLD) {
debouncedLoadMore()
}
})
}, { flush: 'post' })

watch(filteredModules, () => {
isLoading.value = false
Expand Down
1 change: 1 addition & 0 deletions app/pages/video-courses.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ defineOgImage('Docs.takumi', {
:alt="course.name"
:width="'sponsor' in course && course.sponsor ? 94 : 56"
:height="'sponsor' in course && course.sponsor ? 47 : 28"
:sizes="'sponsor' in course && course.sponsor ? '94px' : '56px'"
format="webp"
:modifiers="{ position: 'top' }"
:loading="index > 3 ? 'lazy' : undefined"
Expand Down
2 changes: 1 addition & 1 deletion server/api/admin/mcp-install.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default defineEventHandler(async (event) => {
const { user } = await requireUserSession(event)

if (!user?.login || !(await isAuthorizedAdmin(user.login))) {
if (!user?.login || !(await isAuthorizedAdmin(event, user.login))) {
throw createError({ statusCode: 403, statusMessage: 'Admin access required' })
}

Expand Down
2 changes: 1 addition & 1 deletion server/api/auth/github.get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default defineOAuthGitHubEventHandler({
async onSuccess(event, { user }) {
const allowed = await isAuthorizedAdmin(user.login)
const allowed = await isAuthorizedAdmin(event, user.login)

if (!allowed) {
return sendRedirect(event, '/admin/login?error=access-denied')
Expand Down
6 changes: 3 additions & 3 deletions server/api/v1/teams/index.get.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { GitHubTeamMember } from '../../../types/github'

export default defineCachedEventHandler(async () => {
export default defineCachedEventHandler(async (event) => {
const [core, _ecosystem] = await Promise.all([
$fetch<GitHubTeamMember[]>('/api/v1/teams/core'),
$fetch<GitHubTeamMember[]>('/api/v1/teams/ecosystem')
event.$fetch<GitHubTeamMember[]>('/api/v1/teams/core'),
event.$fetch<GitHubTeamMember[]>('/api/v1/teams/ecosystem')
])

const filteredEcosystem = [] as GitHubTeamMember[]
Expand Down
17 changes: 9 additions & 8 deletions server/utils/team.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { GitHubTeamMember } from '../types/github'
import type { H3Event } from 'h3'

const getCoreMembers = cachedFunction((): Promise<GitHubTeamMember[]> => $fetch<GitHubTeamMember[]>('/api/v1/teams/core'), {
const getCoreMembers = cachedFunction((event: H3Event): Promise<GitHubTeamMember[]> => event.$fetch<GitHubTeamMember[]>('/api/v1/teams/core'), {
maxAge: 60 * 60 * 24 * 7, // 1 week
getKey: () => 'core-members'
})

export async function isCoreTeamMember(login: string): Promise<boolean> {
const coreMembers = await getCoreMembers()
export async function isCoreTeamMember(event: H3Event, login: string): Promise<boolean> {
const coreMembers = await getCoreMembers(event)
if (!coreMembers || !Array.isArray(coreMembers)) {
throw createError({
statusCode: 500,
Expand All @@ -16,19 +17,19 @@ export async function isCoreTeamMember(login: string): Promise<boolean> {
return coreMembers.some(member => member.login.toLowerCase() === login)
}

function getExtraAdminLogins(): string[] {
const raw = useRuntimeConfig().adminGithubLogins
function getExtraAdminLogins(event: H3Event): string[] {
const raw = useRuntimeConfig(event).adminGithubLogins
if (!raw) return []
return raw
.split(',')
.map(login => login.trim().toLowerCase())
.filter(Boolean)
}

export async function isAuthorizedAdmin(login: string): Promise<boolean> {
export async function isAuthorizedAdmin(event: H3Event, login: string): Promise<boolean> {
const normalized = login.toLowerCase()
if (getExtraAdminLogins().includes(normalized)) {
if (getExtraAdminLogins(event).includes(normalized)) {
return true
}
return isCoreTeamMember(normalized)
return isCoreTeamMember(event, normalized)
}