Initialize market simulator from Vue template
This commit is contained in:
parent
3f3a57fbf0
commit
f51a0ef1ae
2
market-simulator/.gitattributes
vendored
Normal file
2
market-simulator/.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
5
market-simulator/.gitignore
vendored
Normal file
5
market-simulator/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
8
market-simulator/.prettierrc
Normal file
8
market-simulator/.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"semi": false
|
||||||
|
}
|
3
market-simulator/.vscode/extensions.json
vendored
Normal file
3
market-simulator/.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["johnsoncodehk.volar"]
|
||||||
|
}
|
35
market-simulator/README.md
Normal file
35
market-simulator/README.md
Normal 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
|
13
market-simulator/index.html
Normal file
13
market-simulator/index.html
Normal 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>
|
22
market-simulator/package.json
Normal file
22
market-simulator/package.json
Normal 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
1
market-simulator/public/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
dump
|
BIN
market-simulator/public/favicon.ico
Normal file
BIN
market-simulator/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
22
market-simulator/src/App.vue
Normal file
22
market-simulator/src/App.vue
Normal 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>
|
BIN
market-simulator/src/assets/logo.png
Normal file
BIN
market-simulator/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
54
market-simulator/src/components/HelloWorld.vue
Normal file
54
market-simulator/src/components/HelloWorld.vue
Normal 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>
|
30
market-simulator/src/components/LoginExample.vue
Normal file
30
market-simulator/src/components/LoginExample.vue
Normal 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
8
market-simulator/src/env.d.ts
vendored
Normal 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
|
||||||
|
}
|
5
market-simulator/src/main.ts
Normal file
5
market-simulator/src/main.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import 'virtual:windi.css'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
22
market-simulator/src/network/example-rules.txt
Normal file
22
market-simulator/src/network/example-rules.txt
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
market-simulator/src/network/init.ts
Normal file
21
market-simulator/src/network/init.ts
Normal 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')
|
||||||
|
}
|
43
market-simulator/src/network/rooms.ts
Normal file
43
market-simulator/src/network/rooms.ts
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
92
market-simulator/src/network/users.ts
Normal file
92
market-simulator/src/network/users.ts
Normal 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)
|
||||||
|
}
|
15
market-simulator/tsconfig.json
Normal file
15
market-simulator/tsconfig.json
Normal 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"]
|
||||||
|
}
|
8
market-simulator/vite.config.ts
Normal file
8
market-simulator/vite.config.ts
Normal 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()],
|
||||||
|
})
|
6
market-simulator/windi.config.ts
Normal file
6
market-simulator/windi.config.ts
Normal 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
1981
market-simulator/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user