Skip to content

Commit

Permalink
feat(login): add <SLoginPage> component
Browse files Browse the repository at this point in the history
  • Loading branch information
NozomuIkuta committed Nov 24, 2023
1 parent a72e35a commit b4da060
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function sidebar(): DefaultTheme.SidebarItem[] {
{ text: 'SInputTextarea', link: '/components/input-textarea' },
{ text: 'SInputYMD', link: '/components/input-ymd' },
{ text: 'SLink', link: '/components/link' },
{ text: 'SLoginPage', link: '/components/login-page' },
{ text: 'SM', link: '/components/m' },
{ text: 'SPill', link: '/components/pill' },
{ text: 'SState', link: '/components/state' },
Expand Down
66 changes: 66 additions & 0 deletions docs/components/login-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SLoginPage

`<SLoginPage>` is the component to render login page.

## Usage

You may use `<SLoginPage>` only for login page.

```vue
<script setup lang="ts">
import SLoginPage from '@globalbrain/sefirot/lib/components/SLoginPage.vue'
</script>
<template>
<SLoginPage />
</template>
```

## Props

Here are the list of props you may pass to the component.

### `:cover`

This prop is the URL of cover image, which is used as background image.

```ts
interface Props {
cover: string
}
```

### `:coverTitle`

This prop is an object whose `text` is the title of the cover image and `link` is its link.

```ts
interface CoverTitle {
text: string
link: string
}
```

### `:coverPhotographer`

This prop is an object whose `text` is the name of photographer of the cover image and `link` is its link.

```ts
interface CoverPhotographer {
text: string
link: string
}
```

### `:actions`

This prop is an array of login buttons,
where `type` is auth provider, `label` is to override default label, and `onClick` is a function to log in.

```ts
interface Action {
type: 'google'
label?: string
onClick: () => Promise<void>
}
```
207 changes: 207 additions & 0 deletions lib/components/SLoginPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<script setup lang="ts">
import { computed } from 'vue'
import SButton from './SButton.vue'
import SLink from './SLink.vue'
import SIconGbLogoWhite from './icon/SIconGbLogoWhite.vue'
import SIconGoogle from './icon/SIconGoogle.vue'
export interface CoverTitle {
text: string
link: string
}
export interface CoverPhotographer {
text: string
link: string
}
export interface Action {
type: 'google'
label?: string
onClick: () => Promise<void>
}
const props = defineProps<{
cover: string
coverTitle: CoverTitle
coverPhotographer: CoverPhotographer
actions: Action[]
}>()
const coverBgImageStyle = computed(() => props.cover ? `url(${props.cover})` : '')
function getActionLabel(type: Action['type']) {
switch (type) {
case 'google':
return 'Sign in via Google'
default:
throw new Error('Invalid action type')
}
}
function getIconComponent(type: Action['type']) {
switch (type) {
case 'google':
return SIconGoogle
default:
throw new Error('Invalid action type')
}
}
</script>

<template>
<div class="SLoginPage">
<div class="cover">
<div class="cover-caption">
<p class="cover-caption-text">
<SLink class="cover-caption-link" :href="coverTitle.link">
{{ coverTitle.text }}
</SLink>
by
<SLink class="cover-caption-link" :href="coverPhotographer.link">
{{ coverPhotographer.text }}
</SLink>
</p>
</div>
</div>
<div class="form">
<div class="form-container">
<div class="form-logo">
<SIconGbLogoWhite class="form-logo-icon" />
</div>

<div class="form-content">
<h1 class="form-title">Sign in to account</h1>
<p class="form-lead">This is a very closed login form meant for specific audiences only. If you can’t login, well, you know who to ask.</p>
</div>

<div class="form-actions">
<SButton
v-for="action in actions"
:key="action.type"
size="large"
mode="white"
rounded
block
:label="action.label || getActionLabel(action.type)"
:icon="getIconComponent(action.type)"
@click="action.onClick"
/>
</div>
</div>
</div>
</div>
</template>

<style scoped lang="postcss">
.SLoginPage {
position: relative;
display: grid;
grid-template-columns: 1fr 480px;
gap: 4px;
background-color: var(--c-black);
@media (min-width: 768px) {
grid-template-columns: 1fr 392px;
}
@media (min-width: 1024px) {
grid-template-columns: 1fr 480px;
}
}
.cover {
width: 100%;
height: 100%;
background-image: v-bind(coverBgImageStyle);
background-position: 50% 50%;
background-size: cover;
background-repeat: no-repeat;
@media (min-width: 768px) {
display: block;
}
}
.cover-caption {
position: absolute;
left: 0;
bottom: 0;
border-top: 4px solid var(--c-bg-elv-1);
border-right: 4px solid var(--c-bg-elv-1);
padding: 16px 24px;
font-size: 12px;
background-color: var(--c-bg-elv-2);
@media (min-width: 768px) {
display: block;
}
}
.cover-caption-text {
color: var(--c-text-2);
}
.cover-caption-link {
color: var(--c-text-1);
transition: color 0.25s;
&:hover {
color: var(--c-text-2);
}
}
.form {
padding: 96px 32px 48px;
min-height: 100vh;
background-color: var(--c-black-soft);
@media (min-width: 768px) {
display: flex;
justify-content: center;
align-items: center;
padding: 48px;
}
}
.form-container {
@media (min-width: 768px) {
margin-top: -96px;
}
}
.form-logo {
margin: 0 auto;
width: 80px;
}
.form-content {
padding-top: 64px;
text-align: center;
}
.form-title {
font-size: 20px;
font-weight: 600;
color: var(--c-text-dark-1);
}
.form-lead {
margin: 0 auto;
padding: 12px;
max-width: 336px;
font-size: 14px;
color: var(--c-text-dark-2);
}
.form-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
max-width: 170px;
padding-top: 24px;
text-align: center;
margin: 0 auto;
}
</style>
13 changes: 13 additions & 0 deletions lib/components/icon/SIconGbLogoWhite.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 192">
<polygon class="line" points="140.49 21.76 140.49 36.09 192 17.32 192 3 140.49 21.76 " />
<polygon class="line" points="42.93 159.04 0 174.68 0 189 42.93 173.36 42.93 159.04 " />
<path class="mark" d="M82.7,57v5.63a24.68,24.68,0,0,0-15.42-5.84c-14.66,0-28.11,11.55-28.11,36,0,27.18,12.22,38.32,27.43,38.32,6.33,0,11.49-2.55,16.06-6.22-.48,19.11-6.5,22.06-24.48,28.61v14.32c23.72-8.65,38.23-13.59,38.23-42.56V52ZM53.16,92.37c0-15.08,5.43-22.69,15.48-22.69,5.45,0,10.57,3.08,14.06,6.42V112c-3.57,3.44-8.29,6.21-13.52,6.21C58.86,118.19,53.16,110.31,53.16,92.37Z " />
<path class="mark" d="M143.47,57.57c-7.32,0-13.17,3.46-18.23,8.13V27.39l-13.58,4.94V129h13.58v-6.53a24.77,24.77,0,0,0,17.57,8c14.51,0,27.55-11.33,27.55-35.31C170.36,68.49,158.52,57.57,143.47,57.57Zm-2,60.22c-6.92,0-13.31-5.06-16.37-9.19V79.15c3.59-4.53,9.31-8.92,15.84-8.92,10.11,0,15.7,7.72,15.7,25.31C156.65,110.33,151.33,117.79,141.48,117.79Z " />
</svg>
</template>

<style scoped lang="postcss">
.line { fill: #979fa4; }
.mark { fill: #ffffff; }
</style>
12 changes: 12 additions & 0 deletions lib/components/icon/SIconGoogle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<svg
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
stroke-linejoin="round"
stroke-miterlimit="1.414"
>
<path d="M8.16 6.857V9.6h4.537c-.183 1.177-1.37 3.45-4.537 3.45-2.73 0-4.96-2.26-4.96-5.05s2.23-5.05 4.96-5.05c1.554 0 2.594.66 3.19 1.233l2.17-2.092C12.126.79 10.32 0 8.16 0c-4.423 0-8 3.577-8 8s3.577 8 8 8c4.617 0 7.68-3.246 7.68-7.817 0-.526-.057-.926-.126-1.326H8.16z" />
</svg>
</template>
Binary file added stories/_img/login-page-cover.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions stories/components/SLoginPage.01_Playground.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import SLoginPage from 'sefirot/components/SLoginPage.vue'
import coverImage from '../_img/login-page-cover.jpg'
const title = 'Components / SLoginPage / 01. Playground'
const docs = '/components/login-page'
function state() {
const state: InstanceType<typeof SLoginPage>['$props'] = {
cover: coverImage,
coverTitle: {
text: 'Golden Gate Bridge',
link: 'https://unsplash.com/photos/bottom-view-of-orange-building-LjE32XEW01g'
},
coverPhotographer: {
text: 'Keegan Houser',
link: 'https://unsplash.com/@khouser01'
},
actions: [
{ type: 'google', onClick: async () => {} },
{ type: 'google', label: 'Sign in via G', onClick: async () => {} }
]
}
return state
}
</script>

<template>
<Story :title="title" :init-state="state" source="Not available" auto-props-disabled>
<template #controls="{ state }">
<HstText
title="cover"
v-model="state.cover"
/>
<HstJson
title="coverTitle"
v-model="state.coverTitle"
/>
<HstJson
title="coverPhotographer"
v-model="state.coverPhotographer"
/>
<HstJson
title="actions"
v-model="state.actions"
/>
</template>

<template #default="{ state }">
<Board :title="title" :docs="docs">
<SLoginPage
:cover="state.cover"
:cover-title="state.coverTitle"
:cover-photographer="state.coverPhotographer"
:actions="state.actions"
/>
</Board>
</template>
</Story>
</template>

0 comments on commit b4da060

Please sign in to comment.