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