Initialize market simulator from Vue template

This commit is contained in:
Austin Chen 2021-11-30 14:22:19 -08:00
parent 3f3a57fbf0
commit f51a0ef1ae
23 changed files with 2396 additions and 0 deletions

2
market-simulator/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

5
market-simulator/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@ -0,0 +1,8 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "es5",
"vueIndentScriptAndStyle": false,
"semi": false
}

View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

View File

@ -0,0 +1,35 @@
# Austin's Starter Project Template
## Usage
1. Clone this repository
2. `yarn`
3. `yarn dev` to start development
4. Setup Firebase
## Setting up Firebase
1. Go to https://console.firebase.google.com/ and create a new project
1. Go to Project Settings and add Firebase to your web app
a. Copy firebaseConfig to `src/network/init.ts`
1. Create a Firestore Database
a. Create a new collection called `users`
b. Set up the security rules (see `src/network/example-rules.txt`)
1. Enable Authetication & Google auth
## Built on top of
- [VueJS](https://v3.vuejs.org/guide/introduction.html) on the frontend
- [Vite](https://vitejs.dev/) for bundling and serving
- [TailwindCSS](https://tailwindcss.com/) for styling
- [WindiCSS](https://windicss.org/) specifically for faster loading times
- [DaisyUI](https://daisyui.com/) for a default set of components
- [Firestore](https://firebase.google.com/docs/firestore) for the database
- [Firebase Auth](https://firebase.google.com/docs/auth) for login
### TODOs:
- [Netlify](https://www.netlify.com/) for hosting
- [Stripe](https://stripe.com/) for payments
- [Mailjet](https://www.mailjet.com/) for marketing & transactional emails

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,22 @@
{
"name": "starter-project",
"version": "0.0.0",
"scripts": {
"dev": "vite --open",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"daisyui": "^1.16.0",
"firebase": "^9.4.1",
"vue": "^3.2.16"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.9.3",
"typescript": "^4.4.3",
"vite": "^2.6.4",
"vite-plugin-windicss": "^1.4.12",
"vue-tsc": "^0.3.0",
"windicss": "^3.2.0"
}
}

1
market-simulator/public/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dump

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'
import LoginExample from './components/LoginExample.vue'
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<LoginExample />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Docs
</a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" class="btn btn-primary" @click="count++">
count is: {{ count }}
</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
firebaseLogin,
firebaseLogout,
listenForLogin,
User,
} from '../network/users'
const user = ref({} as User)
listenForLogin((u) => {
user.value = u
})
function objectEmpty(obj: any) {
// Functional equivalent:
return Object.keys(obj).length === 0
}
</script>
<template>
<div v-if="objectEmpty(user)">
<p>Not logged in!</p>
<button class="btn btn-primary" @click="firebaseLogin">Login</button>
</div>
<div v-else>
<p>Logged in as {{ user.name }}</p>
<button class="btn btn-secondary" @click="firebaseLogout">Logout</button>
</div>
</template>

8
market-simulator/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import 'virtual:windi.css'
createApp(App).mount('#app')

View File

@ -0,0 +1,22 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
// Hardcode admins, e.g.:
return request.auth.uid == 'mvLxCEDq7YSMmatuLZQSxRtsBeh2' || // akrolsmir@gmail.com
request.auth.uid == 'rYFCLWCzSnSjGuSjpLOtWJ1Ewof1' // abc.sinclair@gmail.com
}
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId || isAdmin();
allow create: if request.auth != null || isAdmin();
}
match /rooms/{document=**} {
allow read, create, update: if true;
}
}
}

View File

@ -0,0 +1,21 @@
import { initializeApp } from 'firebase/app'
const firebaseConfig = {
apiKey: 'AIzaSyB1p-PbBT1EcCfJoGtSeZbxlYVvagOoTaY',
authDomain: 'starter-project-7fba2.firebaseapp.com',
projectId: 'starter-project-7fba2',
storageBucket: 'starter-project-7fba2.appspot.com',
messagingSenderId: '751858706800',
appId: '1:751858706800:web:1a69cfbd58c7acbafd87b5',
measurementId: 'G-HPK27K51WM',
}
// Initialize Firebase
export const app = initializeApp(firebaseConfig)
try {
// Note: this is still throwing a console error atm...
import('firebase/analytics').then((analytics) => {
analytics.getAnalytics(app)
})
} catch (e) {
console.warn('Analytics were blocked')
}

View File

@ -0,0 +1,43 @@
import { app } from './init'
import { getFirestore, onSnapshot, doc, updateDoc } from 'firebase/firestore'
import { Unsubscribe } from '@firebase/util'
const db = getFirestore(app)
// Example: Listening for realtime changes to a document
let unsubscribe: Unsubscribe
export function listenRoom(roomId: string, onChange: (room: Room) => void) {
if (unsubscribe) {
unsubscribe()
}
const roomRef = doc(db, 'rooms', roomId)
unsubscribe = onSnapshot(roomRef, (snapshot) => {
const room = snapshot.data() as Room
if (room) {
onChange(room)
}
})
}
// Example: Pushing an update to a document
export async function createMessage(roomId: string, message: Message) {
const roomRef = doc(db, 'rooms', roomId)
await updateDoc(roomRef, {
[`messages.${message.id}`]: message,
[`lastKeystrokes.${message.author.id}`]: message.timestamp,
})
}
export function registerUser(roomId: string, user: Author) {
const roomRef = doc(db, 'rooms', roomId)
return updateDoc(roomRef, {
[`users.${user.id}`]: user,
})
}
export async function registerKeystroke(roomId: string, userId: string) {
const roomRef = doc(db, 'rooms', roomId)
await updateDoc(roomRef, {
[`lastKeystrokes.${userId}`]: Date.now(),
})
}

View File

@ -0,0 +1,92 @@
import { app } from './init'
import { getFirestore, doc, setDoc, getDoc } from 'firebase/firestore'
import { getAuth } from 'firebase/auth'
import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage'
import {
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
} from 'firebase/auth'
export type User = {
id: string
name: string
avatarUrl: string
// Not needed for chat view:
email: string
description: string
createTime: number
lastUpdateTime: number
}
const db = getFirestore(app)
export const auth = getAuth(app)
export async function getUser(userId: string) {
const docSnap = await getDoc(doc(db, 'users', userId))
return docSnap.data() as User
}
export async function setUser(userId: string, user: User) {
await setDoc(doc(db, 'users', userId), user)
}
const CACHED_USER_KEY = 'CACHED_USER_KEY'
export function listenForLogin(onUser: (user: User) => void) {
// Immediately load any persisted user object from browser cache.
const cachedUser = localStorage.getItem(CACHED_USER_KEY)
if (cachedUser) {
onUser(JSON.parse(cachedUser))
}
onAuthStateChanged(auth, async (user) => {
if (user) {
let fetchedUser = await getUser(user.uid)
if (!fetchedUser) {
// User just created an account; save them to our database.
fetchedUser = {
id: user.uid,
name: user.displayName || 'Default Name',
avatarUrl: user.photoURL || '',
email: user.email || 'default@blah.com',
description: '',
createTime: Date.now(),
lastUpdateTime: Date.now(),
}
await setUser(user.uid, fetchedUser)
}
onUser(fetchedUser)
// Persist to local storage, to reduce login blink next time.
// Note: Cap on localStorage size is ~5mb
localStorage.setItem(CACHED_USER_KEY, JSON.stringify(fetchedUser))
} else {
// User logged out; reset to the empty object
onUser({} as User)
}
})
}
export async function firebaseLogin() {
const provider = new GoogleAuthProvider()
signInWithPopup(auth, provider)
}
export async function firebaseLogout() {
auth.signOut()
localStorage.removeItem(CACHED_USER_KEY)
}
const storage = getStorage(app)
// Example: uploadData('avatars/ajfi8iejsf.png', data)
export async function uploadData(
path: string,
data: ArrayBuffer | Blob | Uint8Array
) {
const uploadRef = ref(storage, path)
// Uploaded files should be cached for 1 day, then revalidated
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
const metadata = { cacheControl: 'public, max-age=86400, must-revalidate' }
await uploadBytes(uploadRef, data, metadata)
return await getDownloadURL(uploadRef)
}

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), WindiCSS()],
})

View File

@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import { transform } from 'windicss/helpers'
export default defineConfig({
plugins: [transform('daisyui')],
})

1981
market-simulator/yarn.lock Normal file

File diff suppressed because it is too large Load Diff