fedimovies-web/src/views/LandingPage.vue

491 lines
11 KiB
Vue

<template>
<div class="landing-page wide">
<div class="instance-group">
<div v-if="instance" class="instance-info">
<h1 class="instance-title">{{ instance.title }}</h1>
<div class="instance-description">
{{ instance.short_description }}
<br>
<router-link :to="{ name: 'about' }">Learn more <span class="arrow">&gt;&gt;</span></router-link>
</div>
</div>
<div v-if="instance" class="login-form-group">
<form class="login-form">
<div v-if="isLoading" class="login-form-loader">
<loader></loader>
</div>
<div class="form-control" v-if="!isRegistered || loginType == 'password'">
<div class="input-group">
<input
id="username"
v-model="username"
required
placeholder="Username"
>
<div class="addon">@{{ instance.uri }}</div>
</div>
<div
v-if="!isRegistered"
class="form-message"
:class="{ error: !isUsernameValid() }"
>
Only lowercase letters, numbers and underscores are allowed.
</div>
</div>
<div class="form-control">
<input
id="password"
type="password"
v-model="password"
required
placeholder="Password"
>
</div>
<div class="form-control" v-if="!instance.registrations && !isRegistered">
<input
id="invite-token"
v-model="inviteCode"
required
placeholder="Enter the invite code"
>
</div>
<button
v-if="isRegistered"
type="submit"
:disabled="!isLoginFormValid()"
@click.prevent="login()"
>
Sign in
</button>
<button
v-else
type="submit"
:disabled="!isLoginFormValid()"
@click.prevent="register()"
>
Sign Up
</button>
<div class="error-message" v-if="loginErrorMessage" >{{ loginErrorMessage }}</div>
</form>
<div class="switch-mode">
<template v-if="isRegistered">Don't have an account?</template>
<template v-else>Already registered?</template>
&thinsp;
<button @click.prevent="isRegistered = !isRegistered; loginErrorMessage = null">
<template v-if="isRegistered">Sign Up</template>
<template v-else>Sign In</template>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { watch } from "vue"
import { $, $$, $ref } from "vue/macros"
import { useRouter } from "vue-router"
import {
createUser,
getAccessToken,
getCurrentUser,
} from "@/api/users"
import Loader from "@/components/Loader.vue"
import { useInstanceInfo } from "@/composables/instance"
import { useCurrentUser } from "@/composables/user"
import {
createEip4361_SignedMessage,
getWallet,
hasEthereumWallet,
} from "@/utils/ethereum"
const router = useRouter()
const { setCurrentUser, setAuthToken } = useCurrentUser()
const { getBlockchainInfo, instance } = $(useInstanceInfo())
const isRegistered = $ref(true)
const username = $ref("")
const password = $ref<string | null>(null)
const inviteCode = $ref<string | null>(null)
let loginType = $ref<"password" | "eip4361">("password")
let isLoading = $ref(false)
let loginErrorMessage = $ref<string | null>(null)
function isWalletRequired(): boolean {
return false
}
watch($$(instance), () => {
if (hasEthereumWallet() || isWalletRequired()) {
// Switch to EIP-4361 if wallet is present or
// if registration is token-gated
loginType = "eip4361"
}
}, { immediate: true })
function isUsernameValid(): boolean {
if (!username) {
return true
}
return /^[a-z0-9_]+$/.test(username)
}
function isLoginFormValid(): boolean {
if (!instance) {
return false
}
if (isRegistered) {
if (loginType === "password") {
return Boolean(username) && Boolean(password)
} else {
return true
}
} else {
const inviteCodeValid = instance.registrations ? true : Boolean(inviteCode)
if (!username || !isUsernameValid()) {
return false
}
if (loginType === "password") {
return Boolean(password) && inviteCodeValid
} else {
return inviteCodeValid
}
}
}
async function register() {
loginErrorMessage = null
if (!instance) {
return
}
let userData
let loginData
if (loginType === "password") {
userData = {
username,
password,
message: null,
signature: null,
invite_code: inviteCode,
}
loginData = { username, password, message: null, signature: null }
} else {
const signer = await getWallet()
if (!signer) {
loginErrorMessage = "wallet not found"
return
}
const { message, signature } = await createEip4361_SignedMessage(
signer,
instance.uri,
instance.login_message,
)
userData = {
username,
password: null,
message,
signature,
invite_code: inviteCode,
}
loginData = { username: null, password: null, message, signature }
}
isLoading = true
let user
let authToken
try {
user = await createUser(loginType, userData)
authToken = await getAccessToken(loginType, loginData)
} catch (error: any) {
isLoading = false
loginErrorMessage = error.message
return
}
setCurrentUser(user)
setAuthToken(authToken)
isLoading = false
router.push({ name: "home" })
}
async function login() {
loginErrorMessage = null
if (!instance) {
return
}
let loginData
if (loginType === "password") {
loginData = { username, password, message: null, signature: null }
} else {
const signer = await getWallet()
if (!signer) {
loginErrorMessage = "wallet not found"
return
}
const { message, signature } = await createEip4361_SignedMessage(
signer,
instance.uri,
instance.login_message,
)
loginData = { username: null, password: null, message, signature }
}
isLoading = true
let user
let authToken
try {
authToken = await getAccessToken(loginType, loginData)
user = await getCurrentUser(authToken)
} catch (error: any) {
isLoading = false
loginErrorMessage = error.message
return
}
setCurrentUser(user)
setAuthToken(authToken)
isLoading = false
router.push({ name: "home" })
}
</script>
<style scoped lang="scss">
@import "../styles/layout";
@import "../styles/theme";
$landing-text-color: #fff;
.landing-page {
background-color: #000;
background-image: url("../assets/startpage.png");
background-repeat: no-repeat;
background-size: cover;
box-sizing: border-box;
color: $landing-text-color;
min-height: 100vh;
padding-top: 20vh;
}
.instance-group {
align-items: flex-start;
display: flex;
flex-direction: row;
gap: $content-gap;
justify-content: space-between;
margin: 0 auto;
max-width: $wide-content-width + $content-gap + $wide-sidebar-width;
}
.instance-info {
max-width: $wide-content-width;
min-width: 0;
}
.instance-title {
font-size: 60px;
font-weight: bold;
margin-bottom: 20px;
//text-transform: uppercase;
word-wrap: break-word;
}
.instance-description {
font-size: 24px;
line-height: 1.75;
a {
color: $landing-text-color;
}
.arrow {
color: #7DFF54;
&:hover {
color: $landing-text-color;
}
}
}
.login-form-group {
background-color: #1A1818;
border-radius: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 15px;
max-width: 100%;
min-width: $wide-sidebar-width - 50px;
padding: 30px;
width: $wide-sidebar-width;
}
.login-type {
border-radius: 10px;
display: flex;
button {
border: 1px solid #3D3D3D;
color: #fff;
padding: 10px;
width: 100%;
&:first-child {
border-bottom-left-radius: 10px;
border-top-left-radius: 10px;
}
&:last-child {
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
}
&.active {
background-color: #3D3D3D;
}
}
}
.login-form {
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
input,
.addon {
background-color: #2E2C2C;
border: none;
line-height: 18px;
padding: 15px;
}
input {
border-radius: 10px;
color: $landing-text-color;
min-width: 100px;
&::placeholder {
color: #B3B3B3;
}
}
.input-group {
display: flex;
flex-direction: row;
input {
border-radius: 10px 0 0 10px;
min-width: 0;
}
.addon {
border-radius: 0 10px 10px 0;
color: #B3B3B3;
flex-shrink: 0;
max-width: 40%;
overflow: hidden;
padding-left: 0;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.form-message {
font-size: 12px;
margin-top: 3px;
padding: 0 15px;
&.error {
color: $error-color;
}
}
button[type="submit"] {
background: linear-gradient(to right, #FF5959, #FF5EAD, #D835FE, #D963FF);
background-color: #000;
border: none;
border-radius: 10px;
box-shadow: 0 2px 16px -5px #BB5CC7;
color: $landing-text-color;
cursor: pointer;
display: block;
font-size: 20px;
font-weight: bold;
height: 48px;
padding: 10px 60px;
&:not([disabled]):hover {
background: linear-gradient(to right, #FF7373, #FF78BA, #DD4FFE, #DF7DFF);
}
}
.error-message {
color: $error-color;
margin-top: 10px;
text-align: center;
}
.wallet-required {
align-items: center;
display: flex;
flex-direction: row;
gap: 0.4em;
justify-content: center;
img {
filter: var(--btn-text-colorizer);
height: 1em;
width: 1em;
}
a {
color: $landing-text-color;
text-decoration: underline;
}
}
}
.login-form-loader {
bottom: 0;
display: flex;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 1;
.loader {
margin: auto;
}
}
.switch-mode {
text-align: center;
button {
color: $landing-text-color;
text-decoration: underline;
}
}
@media screen and (max-width: $screen-breakpoint-medium) {
.login-form-group {
padding: 25px;
}
}
@media screen and (max-width: $screen-breakpoint-small) {
.landing-page {
padding-top: $content-gap;
}
.instance-group {
flex-wrap: wrap;
justify-content: flex-start;
}
.login-form-group {
margin-right: auto;
min-width: auto;
}
}
</style>