Skip to content

feat: membershipページを作成 #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
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
13 changes: 13 additions & 0 deletions src/composable/parseRouteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { LocationQueryValue } from 'vue-router'

export const parseRouteQuery = (query: LocationQueryValue | LocationQueryValue[]) => {
if (Array.isArray(query)) {
return query.filter((v): v is string => typeof v === 'string')
} else {
return typeof query === 'string' ? [query] : ['']
}
}

export const parseRedirectQuery = (query: LocationQueryValue | LocationQueryValue[]) => {
return parseRouteQuery(query)[0] || '/'
}
49 changes: 49 additions & 0 deletions src/pages/login/LoginPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { parseRedirectQuery } from '@/composable/parseRouteQuery'
import { useAuthStore } from '@/stores/auth'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const { isLoggedIn } = storeToRefs(authStore)
const { login } = authStore

const redirectTo = parseRedirectQuery(route.query.redirect)

const traqIdInput = ref('')

const handleLogin = () => {
if (traqIdInput.value) {
login(traqIdInput.value)
router.push(redirectTo)
} else {
alert('traQ ID を入力してください。')
}
}

const redirectIfLoggedIn = () => {
if (isLoggedIn.value) {
router.push(redirectTo)
}
}

onMounted(redirectIfLoggedIn)
</script>

<template>
<div v-if="isLoggedIn">
<p>既にログイン済みです。リダイレクトします。</p>
</div>
<div v-else>
<h1>ログイン</h1>
<p>traQ OAuth でログインします。(現在はダミー実装です)</p>
<div>
<label for="traqId">traQ ID:</label>
<input type="text" id="traqId" v-model="traqIdInput" />
</div>
<button @click="handleLogin">ログインする</button>
</div>
</template>
17 changes: 17 additions & 0 deletions src/pages/membership/MemberShipPageUserTypeSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import MemberShipPageUserTypeSelectorButton from './MemberShipPageUserTypeSelectorButton.vue'
</script>

<template>
<div class="flex gap-2">
<MemberShipPageUserTypeSelectorButton userType="new">
新規入部
</MemberShipPageUserTypeSelectorButton>
<MemberShipPageUserTypeSelectorButton userType="rejoin">
再入部
</MemberShipPageUserTypeSelectorButton>
<MemberShipPageUserTypeSelectorButton userType="active">
継続所属
</MemberShipPageUserTypeSelectorButton>
</div>
</template>
19 changes: 19 additions & 0 deletions src/pages/membership/MemberShipPageUserTypeSelectorButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import type { UserType } from './types/userType'

const { userType } = defineProps<{ userType: UserType }>()

const route = useRoute()
const router = useRouter()
const replaceUserTypeQuery = (userType: UserType) => {
const newQuery = { ...route.query, user_type: userType }
router.replace({ query: newQuery })
}
</script>

<template>
<button @click="replaceUserTypeQuery(userType)">
<slot></slot>
</button>
</template>
85 changes: 83 additions & 2 deletions src/pages/membership/MembershipPage.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,86 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
import { useVerifiedEmailStore } from '@/stores/verifiedEmail'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'

import MemberShipPageVerifyEmailLink from '@/pages/membership/MembershipPageVerifyEmailLink.vue'
import { storeToRefs } from 'pinia'
import MemberShipPageUserTypeSelector from './MemberShipPageUserTypeSelector.vue'
import MembershipPageInputText from './MembershipPageInputText.vue'
import MembershipPageLoginLink from './MembershipPageLoginLink.vue'

const route = useRoute()
const { isLoggedIn, loggedInTraqId } = storeToRefs(useAuthStore())

Check failure on line 14 in src/pages/membership/MembershipPage.vue

View workflow job for this annotation

GitHub Actions / Lint

'loggedInTraqId' is assigned a value but never used
const { verifiedEmail, hasVerifiedEmail } = storeToRefs(useVerifiedEmailStore())

Check failure on line 15 in src/pages/membership/MembershipPage.vue

View workflow job for this annotation

GitHub Actions / Lint

'verifiedEmail' is assigned a value but never used

const isUserTypeNew = computed(() => route.query.user_type === 'new')
const isUserTypeRejoin = computed(() => route.query.user_type === 'rejoin')
const isUserTypeActive = computed(() => route.query.user_type === 'active')
const isUserTypeQueryValid = computed(
() => isUserTypeNew.value || isUserTypeRejoin.value || isUserTypeActive.value,
)

const hasCustomerObjectOnTraqId = ref(false)
const shouldShowUserTypeSelector = computed(() => !isLoggedIn.value)
const needUserTypeSelect = computed(() => !isLoggedIn.value && !isUserTypeQueryValid.value)
const needMailAddressInput = computed(
() =>
(isLoggedIn.value && !hasCustomerObjectOnTraqId.value && !hasVerifiedEmail.value) ||
(!isLoggedIn.value &&
(isUserTypeNew.value || isUserTypeRejoin.value) &&
!hasVerifiedEmail.value),
)
const needLogin = computed(() => isUserTypeActive.value && !isLoggedIn.value)

const hasCustomerObjectOnMailAddress = ref(false)
const hasTraQIdOnCustomerObject = ref(false)
const hasCustomerObject = computed(
() => hasCustomerObjectOnMailAddress.value || hasCustomerObjectOnTraqId.value,
)
const canShowInvoiceForm = computed(
() =>
(isLoggedIn.value && (hasCustomerObjectOnTraqId.value || hasVerifiedEmail.value)) ||
(!isLoggedIn.value &&
(isUserTypeNew.value || isUserTypeRejoin.value) &&
hasVerifiedEmail.value),
)
const needTraqIdInput = computed(
() =>
isUserTypeRejoin.value &&
(!hasCustomerObjectOnMailAddress.value || !hasTraQIdOnCustomerObject.value),
)
const traqIdInput = ref('')
const needNameInput = computed(() => !hasCustomerObject.value)
const nameInput = ref('')
const hasValidTraqId = computed(() => !needTraqIdInput.value || traqIdInput.value.length > 0)
const hasName = computed(() => !needNameInput.value || nameInput.value.length > 0)
const canShowInvoiceInfoConfirm = computed(
() => needNameInput.value && hasValidTraqId.value && hasName.value,
)
</script>

<template>
<div>
<h1>This is an membership page</h1>
<div class="flex flex-col gap-2">
<div v-if="shouldShowUserTypeSelector">
<MemberShipPageUserTypeSelector />
</div>
<div v-if="needUserTypeSelect">新規入部、再入部、継続所属のいずれかを選択してください</div>
<div v-if="needMailAddressInput" class="flex flex-col gap-2">
isct アドレスの認証が必要です <MemberShipPageVerifyEmailLink />
</div>
<div v-if="needLogin" class="flex flex-col gap-2">
ログインが必要です
<MembershipPageLoginLink />
</div>
<div v-if="canShowInvoiceForm && needTraqIdInput" class="flex flex-col gap-2">
traQ ID を入力してください
<MembershipPageInputText v-model="traqIdInput" />
</div>
<div v-if="canShowInvoiceForm && needNameInput" class="flex flex-col gap-2">
名前を入力してください
<MembershipPageInputText v-model="nameInput" />
</div>
<div v-if="canShowInvoiceInfoConfirm">請求書発行のための情報を確認してください</div>
</div>
</template>
34 changes: 34 additions & 0 deletions src/pages/membership/MembershipPageInputText.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'

const model = defineModel<string>({ required: true })
const {
autoFocus = false,
className = 'rounded border px-1',
required = false,
placeholder = '',
} = defineProps<{
autoFocus?: boolean
className?: string
required?: boolean
placeholder?: string
}>()

const inputRef = useTemplateRef('input-ref')

onMounted(() => {
if (autoFocus) {
inputRef.value?.focus()
}
})
</script>

<template>
<input
ref="input-ref"
:class="className"
:placeholder="placeholder"
:required="required"
v-model="model"
/>
</template>
13 changes: 13 additions & 0 deletions src/pages/membership/MembershipPageLoginLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
const getLoginPageURL = {
path: '/login',
query: { redirect: route.fullPath },
}
</script>

<template>
<RouterLink :to="getLoginPageURL">ログインへ</RouterLink>
</template>
13 changes: 13 additions & 0 deletions src/pages/membership/MembershipPageVerifyEmailLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
const getVerifyEmailPageURL = {
path: '/verify-email',
query: { redirect: route.fullPath },
}
</script>

<template>
<RouterLink :to="getVerifyEmailPageURL">isct アドレスの認証へ</RouterLink>
</template>
1 change: 1 addition & 0 deletions src/pages/membership/types/userType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type UserType = 'new' | 'rejoin' | 'active'
52 changes: 52 additions & 0 deletions src/pages/verify-email/VerifyEmailPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { parseRedirectQuery } from '@/composable/parseRouteQuery'
import { useVerifiedEmailStore } from '@/stores/verifiedEmail'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
const verifiedEmailStore = useVerifiedEmailStore()
const { hasVerifiedEmail } = storeToRefs(verifiedEmailStore)
const { setVerifiedEmail } = verifiedEmailStore

const redirectTo = parseRedirectQuery(route.query.redirect)

const emailInput = ref('')

const handleVerifyEmail = () => {
// ここで実際のメール送信と検証プロセスをシミュレートします。
// 簡単のため、入力されたメールアドレスを検証済みとして扱います。
if (emailInput.value && emailInput.value.endsWith('@isct.ac.jp')) {
setVerifiedEmail(emailInput.value)
router.push(redirectTo)
} else {
alert('有効な大学のメールアドレス (isct.ac.jp) を入力してください。')
}
}

const redirectIfVerifiedEmailExists = () => {
if (hasVerifiedEmail.value) {
router.push(redirectTo)
}
}

onMounted(redirectIfVerifiedEmailExists)
</script>

<template>
<div v-if="!hasVerifiedEmail">
<h1>メールアドレス認証</h1>
<p>大学のメールアドレス (isct.ac.jp) を入力してください。</p>
<p>メールを送信し、メールアドレスの所有者であることを確認します。(現在はダミー実装です)</p>
<div>
<label for="email">メールアドレス:</label>
<input type="email" id="email" v-model="emailInput" placeholder="[email protected]" />
</div>
<button @click="handleVerifyEmail">認証メールを送信する</button>
</div>
<div v-else>
<p>メールアドレスは既に認証済みです。リダイレクトしています...</p>
</div>
</template>
10 changes: 10 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ const router = createRouter({
// which is lazy-loaded when the route is visited.
component: () => import('@/pages/membership/MembershipPage.vue'),
},
{
path: '/verify-email',
name: 'verify-email',
component: () => import('@/pages/verify-email/VerifyEmailPage.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('@/pages/login/LoginPage.vue'),
},
],
})

Expand Down
19 changes: 19 additions & 0 deletions src/stores/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
import { computed, readonly, ref } from 'vue'

export const useAuthStore = defineStore('auth', () => {
const _loggedInTraqId = ref<{ traqId: string } | null>(null)

const loggedInTraqId = readonly(_loggedInTraqId)
const isLoggedIn = computed(() => _loggedInTraqId.value !== null)

const login = (traqId: string) => {
_loggedInTraqId.value = { traqId }
}

const logout = () => {
_loggedInTraqId.value = null
}

return { isLoggedIn, loggedInTraqId, login, logout }
})
12 changes: 0 additions & 12 deletions src/stores/counter.ts

This file was deleted.

19 changes: 19 additions & 0 deletions src/stores/verifiedEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineStore } from 'pinia'
import { computed, readonly, ref } from 'vue'

export const useVerifiedEmailStore = defineStore('verifiedEmail', () => {
const _verifiedEmail = ref<string | null>(null)

const verifiedEmail = readonly(_verifiedEmail)
const hasVerifiedEmail = computed(() => _verifiedEmail.value !== null)

const setVerifiedEmail = (verifiedEmail: string) => {
_verifiedEmail.value = verifiedEmail
}

const clearEmail = () => {
_verifiedEmail.value = null
}

return { verifiedEmail, hasVerifiedEmail, setVerifiedEmail, clearEmail }
})
Loading