init commit,
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key
|
24
99_references/supabase-examples/user-management/vue3-user-management/.gitignore
vendored
Normal file
24
99_references/supabase-examples/user-management/vue3-user-management/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf"
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
# Supabase Vue 3 User Management
|
||||
|
||||
This repo is a quick sample of how you can get started building apps using Vue 3 and Supabase. You can find a step by step guide of how to build out this app in the [Quickstart: Vue guide](https://supabase.io/docs/guides/with-vue-3).
|
||||
|
||||
This repo will demonstrate how to:
|
||||
|
||||
- sign users in with Supabase Auth using [magic link](https://supabase.io/docs/reference/dart/auth-signin#sign-in-with-magic-link)
|
||||
- store and retrieve data with [Supabase database](https://supabase.io/docs/guides/database)
|
||||
- store image files in [Supabase storage](https://supabase.io/docs/guides/storage)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Before running this app, you need to create a Supabase project and copy [your credentials](https://supabase.io/docs/guides/with-vue-3#get-the-api-keys) to `.env`.
|
||||
|
||||
Run the following command to launch it on `localhost:5173`
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( (select auth.uid()) = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( (select auth.uid()) = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
```
|
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
1682
99_references/supabase-examples/user-management/vue3-user-management/package-lock.json
generated
Normal file
1682
99_references/supabase-examples/user-management/vue3-user-management/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "vue3-user-management",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.0.4",
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Account from './components/Account.vue'
|
||||
import Auth from './components/Auth.vue'
|
||||
import { supabase } from './supabase'
|
||||
|
||||
const session = ref()
|
||||
|
||||
onMounted(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
session.value = data.session
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_, _session) => {
|
||||
session.value = _session
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<Account v-if="session" :session="session" />
|
||||
<Auth v-else />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { supabase } from '../supabase'
|
||||
import { onMounted, ref, toRefs } from 'vue'
|
||||
import Avatar from './Avatar.vue';
|
||||
|
||||
const props = defineProps(['session'])
|
||||
const { session } = toRefs(props)
|
||||
|
||||
const loading = ref(true)
|
||||
const username = ref('')
|
||||
const website = ref('')
|
||||
const avatar_url = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
getProfile()
|
||||
})
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
|
||||
if (data) {
|
||||
username.value = data.username
|
||||
website.value = data.website
|
||||
avatar_url.value = data.avatar_url
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username: username.value,
|
||||
website: website.value,
|
||||
avatar_url: avatar_url.value,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
loading.value = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="form-widget" @submit.prevent="updateProfile">
|
||||
<Avatar v-model:path="avatar_url" @upload="updateProfile" size="10" />
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" :value="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input id="username" type="text" v-model="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" type="url" v-model="website" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" class="button primary block" :value="loading ? 'Loading ...' : 'Update'"
|
||||
:disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" @click="signOut" :disabled="loading">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="row flex-center flex" @submit.prevent="handleLogin">
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + Vue 3</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<div>
|
||||
<input class="inputField" type="email" placeholder="Your email" v-model="email" />
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="button block" :value="loading ? 'Loading' : 'Send magic link'"
|
||||
:disabled="loading" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { ref, toRefs, watch } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
|
||||
const prop = defineProps(['path', 'size'])
|
||||
const { path, size } = toRefs(prop)
|
||||
|
||||
const emit = defineEmits(['upload', 'update:path'])
|
||||
const uploading = ref(false)
|
||||
const src = ref('')
|
||||
const files = ref()
|
||||
|
||||
const downloadImage = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path.value)
|
||||
if (error) throw error
|
||||
src.value = URL.createObjectURL(data)
|
||||
} catch (error) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (evt) => {
|
||||
files.value = evt.target.files
|
||||
try {
|
||||
uploading.value = true
|
||||
if (!files.value || files.value.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = files.value[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
|
||||
if (uploadError) throw uploadError
|
||||
emit('update:path', filePath)
|
||||
emit('upload')
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(path, () => {
|
||||
if (path.value) downloadImage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<img v-if="src" :src="src" alt="Avatar" class="avatar image"
|
||||
:style="{ height: size + 'em', width: size + 'em' }" />
|
||||
<div v-else class="avatar no-image" :style="{ height: size + 'em', width: size + 'em' }" />
|
||||
|
||||
<div :style="{ width: size + 'em' }">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input style="visibility: hidden; position: absolute" type="file" id="single" accept="image/*"
|
||||
@change="uploadAvatar" :disabled="uploading" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
@@ -0,0 +1,5 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const store = reactive({
|
||||
user: {},
|
||||
})
|
@@ -0,0 +1,372 @@
|
||||
html,
|
||||
body {
|
||||
--custom-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--custom-bg-color: #101010;
|
||||
--custom-panel-color: #222;
|
||||
--custom-box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.8);
|
||||
--custom-color: #fff;
|
||||
--custom-color-brand: #24b47e;
|
||||
--custom-color-secondary: #666;
|
||||
--custom-border: 1px solid #333;
|
||||
--custom-border-radius: 5px;
|
||||
--custom-spacing: 5px;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: var(--custom-font-family);
|
||||
background-color: var(--custom-bg-color);
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--custom-color);
|
||||
font-family: var(--custom-font-family);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#__next {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.row [class^="col"] {
|
||||
float: left;
|
||||
margin: 0.5rem 2%;
|
||||
min-height: 0.125rem;
|
||||
}
|
||||
.col-1,
|
||||
.col-2,
|
||||
.col-3,
|
||||
.col-4,
|
||||
.col-5,
|
||||
.col-6,
|
||||
.col-7,
|
||||
.col-8,
|
||||
.col-9,
|
||||
.col-10,
|
||||
.col-11,
|
||||
.col-12 {
|
||||
width: 96%;
|
||||
}
|
||||
.col-1-sm {
|
||||
width: 4.33%;
|
||||
}
|
||||
.col-2-sm {
|
||||
width: 12.66%;
|
||||
}
|
||||
.col-3-sm {
|
||||
width: 21%;
|
||||
}
|
||||
.col-4-sm {
|
||||
width: 29.33%;
|
||||
}
|
||||
.col-5-sm {
|
||||
width: 37.66%;
|
||||
}
|
||||
.col-6-sm {
|
||||
width: 46%;
|
||||
}
|
||||
.col-7-sm {
|
||||
width: 54.33%;
|
||||
}
|
||||
.col-8-sm {
|
||||
width: 62.66%;
|
||||
}
|
||||
.col-9-sm {
|
||||
width: 71%;
|
||||
}
|
||||
.col-10-sm {
|
||||
width: 79.33%;
|
||||
}
|
||||
.col-11-sm {
|
||||
width: 87.66%;
|
||||
}
|
||||
.col-12-sm {
|
||||
width: 96%;
|
||||
}
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.hidden-sm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 33.75em) {
|
||||
/* 540px */
|
||||
.container {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 45em) {
|
||||
/* 720px */
|
||||
.col-1 {
|
||||
width: 4.33%;
|
||||
}
|
||||
.col-2 {
|
||||
width: 12.66%;
|
||||
}
|
||||
.col-3 {
|
||||
width: 21%;
|
||||
}
|
||||
.col-4 {
|
||||
width: 29.33%;
|
||||
}
|
||||
.col-5 {
|
||||
width: 37.66%;
|
||||
}
|
||||
.col-6 {
|
||||
width: 46%;
|
||||
}
|
||||
.col-7 {
|
||||
width: 54.33%;
|
||||
}
|
||||
.col-8 {
|
||||
width: 62.66%;
|
||||
}
|
||||
.col-9 {
|
||||
width: 71%;
|
||||
}
|
||||
.col-10 {
|
||||
width: 79.33%;
|
||||
}
|
||||
.col-11 {
|
||||
width: 87.66%;
|
||||
}
|
||||
.col-12 {
|
||||
width: 96%;
|
||||
}
|
||||
.hidden-sm {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 60em) {
|
||||
/* 960px */
|
||||
.container {
|
||||
width: 75%;
|
||||
max-width: 60rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
color: var(--custom-color-secondary);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
border: var(--custom-border);
|
||||
padding: 8px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--custom-bg-color);
|
||||
color: var(--custom-color);
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
color: var(--custom-color-secondary);
|
||||
}
|
||||
|
||||
/* Utils */
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flex.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex.flex-1 {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.flex-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.flex-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.text-sm {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.font-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
.opacity-half {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
|
||||
button,
|
||||
.button {
|
||||
color: var(--custom-color);
|
||||
border: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button.primary,
|
||||
.button.primary {
|
||||
background-color: var(--custom-color-brand);
|
||||
border: 1px solid var(--custom-color-brand);
|
||||
}
|
||||
|
||||
/* Widgets */
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
padding: var(--custom-spacing);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: var(--custom-border-radius);
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
.avatar.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
.avatar.no-image {
|
||||
background-color: #333;
|
||||
border: 1px solid rgb(200, 200, 200);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
border-top: var(--custom-border);
|
||||
background-color: var(--custom-bg-color);
|
||||
}
|
||||
.footer div {
|
||||
padding: var(--custom-spacing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.footer div > img {
|
||||
height: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.footer > div:first-child {
|
||||
display: none;
|
||||
}
|
||||
.footer > div:nth-child(2) {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 60em) {
|
||||
/* 960px */
|
||||
.footer > div:first-child {
|
||||
display: flex;
|
||||
}
|
||||
.footer > div:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mainHeader {
|
||||
width: 100%;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatarPlaceholder {
|
||||
border: var(--custom-border);
|
||||
border-radius: var(--custom-border-radius);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-widget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-widget > .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background-color: #444444;
|
||||
text-transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-widget .button:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.form-widget .button > .loader {
|
||||
width: 17px;
|
||||
animation: spin 1s linear infinite;
|
||||
filter: invert(1);
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()]
|
||||
})
|
Reference in New Issue
Block a user