├── pages ├── loading.vue ├── post │ ├── index.vue │ └── _post │ │ ├── embed.vue │ │ └── index.vue ├── topic │ ├── index.vue │ └── _topic.vue ├── user │ ├── index.vue │ └── _username.vue ├── browse │ ├── index.vue │ └── _id.vue ├── docs │ ├── index.vue │ └── _slug.vue ├── youtube │ └── _id.vue ├── starred.vue ├── login.vue ├── index.vue ├── confirm-login.vue ├── react │ └── _id.vue ├── banner.vue ├── dashboard │ ├── index.vue │ └── users.vue └── search.vue ├── bun.lockb ├── static ├── favicon.ico ├── icon white.png ├── emojis │ ├── ocular.png │ └── squirrel.png ├── reaction-screenshot.png ├── original icon.svg └── icon.svg ├── assets ├── banner-blank.png ├── README.md └── category-map.js ├── plugins ├── vue-tooltip.js ├── vue-good-table.js ├── README.md └── auth.js ├── components ├── Emoji.vue ├── PostCount.vue ├── PostTime.vue ├── PostList.vue ├── Status.vue ├── TopicTime.vue ├── TopicList.vue ├── Error.vue ├── TopicListItem.vue ├── Star.vue ├── Footer.vue ├── Header.vue ├── ForumSelector.vue ├── Loading.vue ├── Post.vue ├── ReactionButtons.vue └── Render.vue ├── middleware ├── README.md ├── admin.js ├── notauthenticated.js └── authenticated.js ├── store ├── README.md ├── index.js ├── counts.js ├── statuses.js └── auth.js ├── content └── docs │ ├── gallery.md │ ├── about.md │ ├── privacy.md │ └── api.md ├── package.json ├── .github └── ISSUE_TEMPLATE │ ├── ------feature-request.md │ └── ---bug-report.md ├── README.md ├── .gitignore ├── nuxt.config.js └── layouts └── default.vue /pages/loading.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/bun.lockb -------------------------------------------------------------------------------- /pages/post/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/topic/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/user/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/icon white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/icon white.png -------------------------------------------------------------------------------- /assets/banner-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/assets/banner-blank.png -------------------------------------------------------------------------------- /static/emojis/ocular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/emojis/ocular.png -------------------------------------------------------------------------------- /static/emojis/squirrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/emojis/squirrel.png -------------------------------------------------------------------------------- /plugins/vue-tooltip.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VTooltip from 'v-tooltip' 3 | 4 | Vue.use(VTooltip) -------------------------------------------------------------------------------- /static/reaction-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffalo/ocular/HEAD/static/reaction-screenshot.png -------------------------------------------------------------------------------- /components/Emoji.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /plugins/vue-good-table.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueGoodTablePlugin from 'vue-good-table'; 3 | 4 | // import the styles 5 | import 'vue-good-table/dist/vue-good-table.css' 6 | 7 | Vue.use(VueGoodTablePlugin); -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /plugins/auth.js: -------------------------------------------------------------------------------- 1 | export default ({store}, inject) => { 2 | // Inject $hello(msg) in Vue, context and store. 3 | let auth = { 4 | loggedIn() { 5 | return !!store.state.auth.user 6 | }, 7 | user() { 8 | return store.state.auth.user 9 | }, 10 | token() { 11 | return store.state.auth.token 12 | } 13 | } 14 | inject('auth', auth) 15 | } -------------------------------------------------------------------------------- /static/original icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/browse/index.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /static/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /components/PostCount.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /components/PostTime.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /content/docs/gallery.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ocular api usage gallery 3 | headertitle: api gallery 4 | description: how are people using the ocular api? 5 | --- 6 | 7 | - a browser extension. i wish i could tell you which one, but [no](/topic/284272). 8 | - [Scratch Forum Leaderboards](https://shefwerld.rirurin.com/post/) 9 | - [postpercent](https://postpercent.rirurin.com/) 10 | - [Scratory](https://scratory.vercel.app/) 11 | - [Scratch Tools](https://scratchtools.edu.eu.org/) 12 | - [Magnifier](https://magnifier.potatophant.net/) 13 | 14 | want to add something? [edit this page](https://github.com/jeffalo/ocular/blob/main/content/docs/gallery.md) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocular", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate" 10 | }, 11 | "dependencies": { 12 | "@nuxt/content": "^1.15.1", 13 | "@nuxtjs/redirect-module": "^0.3.1", 14 | "cookie": "^0.4.2", 15 | "core-js": "^3.37.1", 16 | "js-cookie": "^2.2.1", 17 | "nuxt": "^2.17.4", 18 | "v-tooltip": "^2.1.3", 19 | "vue-good-table": "^2.21.11", 20 | "vue-plausible": "^1.3.2" 21 | }, 22 | "devDependencies": { 23 | "@nuxtjs/color-mode": "^2.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /components/PostList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/------feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F468‍\U0001F4BB feature request" 3 | about: do you have a new feature for ocular or my ocular? if so this is the template 4 | for you. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /middleware/admin.js: -------------------------------------------------------------------------------- 1 | const serverCookie = process.server ? require('cookie') : undefined 2 | const clientCookie = process.client ? require('js-cookie') : undefined 3 | 4 | // make sure to keep this the same as authetnicated.js 5 | 6 | export default async function ({ $auth, redirect, req, store }) { 7 | let token = null 8 | if (process.server) { 9 | if (req.headers.cookie) { 10 | const parsed = serverCookie.parse(req.headers.cookie) 11 | token = parsed['my-ocular-token'] 12 | } 13 | } else { 14 | token = clientCookie.get('my-ocular-token') 15 | } 16 | 17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something 18 | 19 | if (!$auth.user().admin) { 20 | return redirect('/') 21 | } 22 | } -------------------------------------------------------------------------------- /pages/docs/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /middleware/notauthenticated.js: -------------------------------------------------------------------------------- 1 | const serverCookie = process.server ? require('cookie') : undefined 2 | const clientCookie = process.client ? require('js-cookie') : undefined 3 | 4 | // make sure to keep this the same as authetnicated.js 5 | 6 | export default async function ({ $auth, redirect, req, store }) { 7 | let token = null 8 | if (process.server) { 9 | if (req.headers.cookie) { 10 | const parsed = serverCookie.parse(req.headers.cookie) 11 | token = parsed['my-ocular-token'] 12 | } 13 | } else { 14 | token = clientCookie.get('my-ocular-token') 15 | } 16 | 17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something 18 | 19 | if ($auth.loggedIn()) { 20 | return redirect('/') 21 | } 22 | } -------------------------------------------------------------------------------- /middleware/authenticated.js: -------------------------------------------------------------------------------- 1 | const serverCookie = process.server ? require('cookie') : undefined 2 | const clientCookie = process.client ? require('js-cookie') : undefined 3 | 4 | // make sure to keep this the same as notauthetnicated.js 5 | 6 | export default async function ({ $auth, redirect, req, store }) { 7 | let token = null 8 | if (process.server) { 9 | if (req.headers.cookie) { 10 | const parsed = serverCookie.parse(req.headers.cookie) 11 | token = parsed['my-ocular-token'] 12 | } 13 | } else { 14 | token = clientCookie.get('my-ocular-token') 15 | } 16 | 17 | await store.dispatch('auth/login', token) // reload just incase logged out on another tab or something 18 | 19 | if (!$auth.loggedIn()) { 20 | return redirect('/login') 21 | } 22 | } -------------------------------------------------------------------------------- /content/docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: about 3 | description: about ocular 4 | --- 5 | 6 | ## what is ocular? 7 | ocular is a tool for searching the [scratch forums](https://scratch.mit.edu/discuss/). it contains some cool features like statuses, reactions and starring to make using the scratch forums more fun! 8 | 9 | ## what is/was my-ocular? 10 | my-ocular previously was a site where you could set your ocular status. for technical reasons this had to be maintained separately from ocular, however in april 2021, all of my-ocular's features were migrated to ocular. my-ocular only remains as a backend/api. 11 | 12 | ## what is an ocular status? 13 | an ocular status is a short message and favourite colour, which is displayed across ocular and [other sites](/docs/gallery). an ocular status is not required to use ocular, but they allow you to customize your profile a little bit. :) 14 | 15 | [set your ocular status](/dashboard) 16 | -------------------------------------------------------------------------------- /components/Status.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /components/TopicTime.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /pages/youtube/_id.vue: -------------------------------------------------------------------------------- 1 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /components/TopicList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B bug report" 3 | about: are you facing an issue with ocular or my ocular? if so this is the template 4 | for you. 5 | title: '' 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Browser [e.g. stock browser, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /components/Error.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | -------------------------------------------------------------------------------- /components/TopicListItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔍 ocular 2 | ocular is the search tool for the scratch forums. 3 | 4 | it's up at [ocular.jeffalo.net](https://ocular.jeffalo.net) 5 | 6 | ## behind the scenes 7 | - data from [DatOneLefty's ScratchDB](https://scratchdb.lefty.one/) 8 | - styling taken from [Maximouse](https://scratch.mit.edu/users/Maximouse)'s userstyle 9 | - big inspiration from [forums.scratchstats.com](https://forums.scratchstats.com) 10 | - nuxt because SSR 11 | - hosted at home 12 | - and obviously scratch! 13 | 14 | ## how to run 15 | 16 | you will need your own my-ocular server if you don't want to use production data, otherwise you can use the production my-ocular server by setting the `BACKEND_URL` env variable to `https://my-ocular.jeffalo.net`. keep in mind you will get cors errors unless you run the nuxt server on `localhost` port `8000` or `8001`. you can change the nuxt server port via the `PORT` env variable. 17 | 18 | ```bash 19 | # install dependencies 20 | $ npm install 21 | 22 | # serve with hot reload at localhost:3000 23 | $ npm run dev 24 | 25 | # build for production and launch server 26 | $ npm run build 27 | $ npm run start 28 | 29 | # generate static project 30 | $ npm run generate 31 | ``` 32 | -------------------------------------------------------------------------------- /pages/starred.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /components/Star.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 46 | 47 | -------------------------------------------------------------------------------- /pages/docs/_slug.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 47 | 48 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | const cookie = process.server ? require('cookie') : undefined 2 | 3 | export const actions = { 4 | async nuxtServerInit({ commit }, { req, res }) { 5 | // handle auth stuff 6 | 7 | let token = null 8 | if (req.headers.cookie) { 9 | const parsed = cookie.parse(req.headers.cookie) 10 | try { 11 | token = parsed['my-ocular-token'] 12 | } catch (error) { 13 | console.log(error) 14 | } 15 | } 16 | // if the cookie exists, then get user data 17 | if (token !== null && token !== false) { 18 | await fetch(`${process.env.backendURL}/auth/me`, { 19 | headers: { 20 | 'Authorization': token 21 | } 22 | }) 23 | .then(res => res.json()) 24 | .then((data) => { 25 | if (data.error) { 26 | // if theres an error (token invalid or user deleted, log the user out and remove the token) 27 | commit('auth/reset_user', null) 28 | commit('auth/set_token', null) 29 | res.setHeader('Set-Cookie', [`my-ocular-token=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`]) 30 | } else { 31 | // if data, save to store 32 | commit('auth/set_user', data) 33 | commit('auth/set_token', token) 34 | } 35 | }).catch((error) => { 36 | console.warn(error) 37 | }) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /pages/post/_post/embed.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 64 | 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | 92 | # update.sh file 93 | update.sh -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | -------------------------------------------------------------------------------- /store/counts.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | users: [] 3 | }) 4 | 5 | let findUser = (state, name) => { 6 | return state.users.find(user => user.name.toLowerCase() == name.toLowerCase()) 7 | } 8 | 9 | export const actions = { 10 | async loadUser({ state, commit }, { name }) { 11 | let found = findUser(state, name) 12 | if (found && !found.loading) { 13 | // its ready, just send it 14 | return found.count 15 | } 16 | 17 | if (found && found.loading) { 18 | // its loading, return the promise 19 | return found.promise 20 | } 21 | 22 | // this is the first time 23 | 24 | let promise = new Promise((resolve, reject) => { 25 | fetch("https://scratchdb.lefty.one/v3/forum/user/info/" + name) 26 | .then((res) => res.json()) 27 | .then(data => { 28 | resolve(data.counts.total.count) 29 | commit('setUser', { name, count: data.counts.total.count, promise, loading: false }) 30 | }) 31 | }); 32 | 33 | 34 | commit('initUser', { name, promise, loading: true }) 35 | 36 | return promise 37 | } 38 | } 39 | 40 | export const mutations = { 41 | setUser(state, { name, count, promise, loading }) { 42 | let found = findUser(state, name) 43 | if (found) { 44 | found = { name, count, promise, loading } 45 | } else { 46 | state.users.push({ name, count, promise, loading }) 47 | } 48 | }, 49 | initUser(state, { name, promise } ) { 50 | let found = findUser(state, name) 51 | if (!found) { 52 | state.users.push({ name, count: 0, promise, loading: true }) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /pages/post/_post/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 68 | 69 | -------------------------------------------------------------------------------- /content/docs/privacy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: privacy 3 | description: ocular privacy stuff. 4 | --- 5 | 6 | *TLDR: ocular only collects basic analytics data, registered user data and data that powers features such as reactions and starring. I don't want your private info.* 7 | 8 | ocular uses a number of services to provide various tools to enhance the experience. These services may collect and or use data according to their privacy policies. 9 | 10 | - To collect basic usage analytics, ocular uses a self-hosted instance of [Plausible Analytics](https://plausible.io). You can find out more about what kind of information is collected at the [Plausible Analytics website](https://plausible.io). 11 | 12 | - ocular uses [CloudFlare](https://cloudflare.com) to proxy requests, this keeps the site running smoothly and allows me to host ocular right from my home. 13 | 14 | - Data about Scratch forum posts comes from [ScratchDB](https://scratchdb.lefty.one), and Scratch based authentication is handled by [Scratch Auth](https://auth.itinerary.eu.org/). All other Scratch data comes straight from [the official Scratch API](https://github.com/llk/scratch-rest-api). 15 | 16 | - ocular does not store any Scratch data. ocular's purpose is to present data from the APIs listed above. Outside of analytics, ocular only stores registered user data, and data to power reactions and starring. User accounts are not required to use the base functionality of ocular. 17 | 18 | - ocular (and my other projects) are hosted on a server running NGINX. For security purposes, NGINX logs may include IP addresses. 19 | 20 | - If you would like data deletion from ocular, contact me at `jeffalobob at gmail` 21 | 22 | - For external services, please consult their sites for information about how data is stored and used, if you have any questions about your privacy when using ocular feel free to contact me about it. 23 | -------------------------------------------------------------------------------- /store/statuses.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | users: [] 3 | }) 4 | 5 | let findUser = (state, name) => { 6 | return state.users.find(user => user.name.toLowerCase() == name.toLowerCase()) 7 | } 8 | 9 | export const actions = { 10 | async loadUser({ state, commit }, { name }) { 11 | let found = findUser(state, name) 12 | if (found && !found.loading) { 13 | // its ready, just send it 14 | return found.data 15 | } 16 | 17 | if (found && found.loading) { 18 | // its loading, return the promise 19 | return found.promise 20 | } 21 | 22 | // this is the first time 23 | 24 | let promise = new Promise((resolve, reject) => { 25 | fetch(`${process.env.backendURL}/api/user/` + name) 26 | .then((res) => res.json()) 27 | .then(data => { 28 | resolve(data) 29 | commit('setUser', { name, data, promise, loading: false }) 30 | }) 31 | }); 32 | 33 | 34 | commit('initUser', { name, promise, loading: true }) 35 | 36 | return promise 37 | } 38 | } 39 | 40 | export const mutations = { 41 | setUser(state, { name, data, promise, loading }) { 42 | let found = findUser(state, name) 43 | if (found) { 44 | found = { name, data, promise, loading } 45 | } else { 46 | state.users.push({ name, data, promise, loading }) 47 | } 48 | }, 49 | removeUser(state, { name }) { 50 | state.users = state.users.filter(user => { 51 | return user.name !== name; 52 | }) 53 | }, 54 | initUser(state, { name, promise } ) { 55 | let found = findUser(state, name) 56 | if (!found) { 57 | state.users.push({ name, data: {}, promise, loading: true }) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /pages/browse/_id.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 75 | -------------------------------------------------------------------------------- /store/auth.js: -------------------------------------------------------------------------------- 1 | import cookies from 'js-cookie' 2 | 3 | export const state = () => ({ 4 | user: null, 5 | token: null 6 | }) 7 | 8 | export const mutations = { 9 | set_user(store, data) { 10 | store.user = data 11 | }, 12 | set_token(store, data) { 13 | store.token = data 14 | }, 15 | reset_user(store) { 16 | store.user = null 17 | }, 18 | reset_token(store, data) { 19 | store.token = null 20 | } 21 | } 22 | 23 | export const actions = { 24 | async login({ commit, dispatch }, token) { // this is also used as a general purpose "refresh user details" in middlewares and dashboard.vue 25 | return new Promise((resolve, reject) => { 26 | fetch(`${process.env.backendURL}/auth/me`, { 27 | headers: { 28 | Authorization: token 29 | } 30 | }) 31 | .then(res => res.json()) 32 | .then(data => { 33 | if (data.error) { 34 | // console.warn(data.error) 35 | dispatch('logout') 36 | resolve(data.error) // i could use reject but i'd need to handle it everywhere and i dont feel like doing that 37 | } else { 38 | commit('set_user', data) 39 | commit('set_token', token) 40 | resolve(data) 41 | } 42 | }) 43 | }) 44 | }, 45 | async logout({ commit }) { 46 | return new Promise(async (resolve, reject) => { 47 | let token = cookies.get('my-ocular-token') 48 | let res = await fetch(`${process.env.backendURL}/auth/remove/?token=${token}`, { 49 | method: "POST" 50 | }) 51 | let data = await res.json() 52 | 53 | cookies.remove('my-ocular-token') 54 | commit('reset_user') 55 | commit('reset_token') 56 | 57 | if(data.error){ 58 | resolve(data.error) // i could use reject but i'd need to handle it everywhere and i dont feel like doing that 59 | } else { 60 | resolve('logged out') 61 | } 62 | }) 63 | } 64 | } -------------------------------------------------------------------------------- /components/Footer.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 64 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Global page headers (https://go.nuxtjs.dev/config-head) 3 | head: { 4 | titleTemplate: (titleChunk) => { 5 | return titleChunk ? `ocular | ${titleChunk}` : 'ocular'; 6 | }, 7 | meta: [ 8 | { charset: 'utf-8' }, 9 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 10 | { hid: 'description', name: 'description', content: 'scratch forum search and more!' } 11 | ], 12 | script: [ 13 | { 14 | src: '/lib/scratchblocks.min.js' 15 | } 16 | ], 17 | link: [ 18 | { rel: 'icon', type: 'image/x-icon', href: '/icon.svg' } 19 | ] 20 | }, 21 | 22 | // Global CSS (https://go.nuxtjs.dev/config-css) 23 | css: [ 24 | ], 25 | 26 | // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins) 27 | plugins: ['~/plugins/auth.js', { src: '~/plugins/vue-good-table', ssr: false }, '~/plugins/vue-tooltip.js'], 28 | 29 | // Auto import components (https://go.nuxtjs.dev/config-components) 30 | components: true, 31 | 32 | // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules) 33 | buildModules: [ 34 | '@nuxtjs/color-mode' 35 | ], 36 | 37 | // Modules (https://go.nuxtjs.dev/config-modules) 38 | modules: [ 39 | 'vue-plausible', 40 | '@nuxtjs/redirect-module', 41 | '@nuxt/content' 42 | ], 43 | plausible: { 44 | domain: 'ocular.jeffalo.net', 45 | apiHost: 'https://analytics.jeffalo.net' 46 | }, 47 | 48 | redirect: [ 49 | { from: '^/discuss/(.*)$', to: '/$1' }, 50 | { from: '^/users/(.*)$', to: '/user/$1' }, 51 | { from: '^/@(.*)$', to: '/user/$1' }, 52 | { from: '^/u/(.*)$', to: '/user/$1' }, 53 | { from: '^/t/(.*)$', to: '/topic/$1' }, 54 | { from: '^/p/(.*)$', to: '/post/$1' }, 55 | { from: '^/privacy', to: '/docs/privacy' }, 56 | { from: '^/about', to: '/docs/about' }, 57 | { from: '^/admin', to: 'https://jeffalo.net/internal/admin/' } 58 | ], 59 | 60 | env: { 61 | backendURL: process.env.BACKEND_URL || 'http://localhost:8081' 62 | }, 63 | 64 | // Build Configuration (https://go.nuxtjs.dev/config-build) 65 | build: { 66 | transpile: ['vue-good-table'] 67 | }, 68 | 69 | loading: { 70 | color: 'white', 71 | height: '2px' 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /pages/confirm-login.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 69 | 70 | -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 25 | 61 | 62 | 76 | -------------------------------------------------------------------------------- /assets/category-map.js: -------------------------------------------------------------------------------- 1 | export default new Map( 2 | // i don't think you understand the pain i went through to create this list.. feel free to use it for whatever with credit (to jeffalo) though. 3 | [ 4 | ["1", "Suggestions"], 5 | ["2", "Dustbin"], 6 | ["3", "Bugs and Glitches"], 7 | ["4", "Questions about Scratch"], 8 | ["5", "Announcements"], 9 | ["6", "New Scratchers"], 10 | ["7", "Help With Scripts"], 11 | ["8", "Show and Tell"], 12 | ["9", "Project Ideas"], 13 | ["10", "Collaboration"], 14 | ["11", "Requests"], 15 | ["12", "Moderators"], 16 | ["13", "Deutsch"], 17 | ["14", "Español"], 18 | ["15", "Français"], 19 | ["16", "中文"], 20 | ["17", "Polski"], 21 | ["18", "日本語"], 22 | ["19", "Nederlands"], 23 | ["20", "Português"], 24 | ["21", "Italiano"], 25 | ["22", "עברית"], 26 | ["23", "한국어"], 27 | ["24", "Norsk"], 28 | ["25", "Türkçe"], 29 | ["26", "Ελληνικά"], 30 | ["27", "Pусский"], 31 | ["28", "Translating Scratch"], 32 | ["29", "Things I'm Making and Creating"], 33 | ["30", "Things I'm Reading and Playing"], 34 | ["31", "Advanced Topics"], 35 | ["32", "Connecting to the Physical World"], 36 | ["33", "Català"], 37 | ["34", "Other Languages"], 38 | ["35", "Mentors Forum"], 39 | ["36", "Bahasa Indonesia"], 40 | ["37", "Scratch Day 2014"], 41 | ["38", "Spam Dustbin"], 42 | ["39", "Scratch Helper Groups"], 43 | ["40", "Camp Counselor Forum"], 44 | ["41", "Extension Developer's Forum "], 45 | ["42", "Scratch Stability Team Forum"], 46 | // 43 returns 404 47 | ["44", "Scratch Day 2015"], 48 | // 45 returns 404 49 | ["46", "Scratch Design Studio Forum "], 50 | // 47 returns 404 51 | ["48", "Developing Scratch Extensions"], 52 | ["49", "Open Source Projects"], 53 | ["50", "Welcoming Committee"], 54 | ["51", "Community Blocks Forum"], 55 | ["52", "Scratch Day 2016"], 56 | // ["53", "[unknown to the rest of the world 😉]"], 57 | ["54", "Scratch Day 2017"], 58 | ["55", "Africa"], 59 | ["56", "Scratch Day 2018"], 60 | ["57", "Scratch 3.0 Beta"], 61 | ["58", "Camp Counselors 2020"], 62 | ["59", "فارسی"], 63 | ["60", "Project Save & Level Codes"], 64 | ["61", "April Fools Day - Suggest-Show-Question-Bugs-Help-Glitch-Tell-Etc"], 65 | ] 66 | ); 67 | -------------------------------------------------------------------------------- /components/ForumSelector.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 75 | 76 | 81 | -------------------------------------------------------------------------------- /pages/react/_id.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /pages/banner.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | -------------------------------------------------------------------------------- /pages/topic/_topic.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 122 | 123 | -------------------------------------------------------------------------------- /pages/user/_username.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 103 | 104 | 136 | 137 | -------------------------------------------------------------------------------- /content/docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API documentation 3 | headertitle: api 4 | description: ocular api documentation. 5 | --- 6 | 7 | ocular's api is integrated into a [handful of scratch-related projects](/docs/gallery). that's pretty cool in my opinion. 8 | 9 | the public ocular api is designed for retrieving data, not for setting it. in the future i might include some api token stuff, but that gets complicated and un-fun. please don't use the internal api endpoints (the ones not documented here) to update data automatically. it makes moderation harder. 10 | 11 | all api endpoints should be assumed to be under `my-ocular.jeffalo.net` unless otherwise noted. 12 | 13 | ## get an ocular user 14 | 15 | make a GET request to `/api/user/:username` 16 | 17 | you can use the query parameter `?noReplace=true` to retrieve raw status data (without replacing the elements described later) 18 | 19 | ### example response 20 | 21 | (from `https://my-ocular.jeffalo.net/api/user/Jeffalo`) 22 | 23 | ```json 24 | { 25 | "_id": "5fb91a89532f943b9046e3ba", 26 | "name": "Jeffalo", 27 | "status": "{joke}", 28 | "color": "#0fbd8c", 29 | "admin": true, 30 | "meta": { 31 | "updated": "2021-05-09T13:24:30.021Z", 32 | "updatedBy": "Jeffalo" 33 | } 34 | } 35 | ``` 36 | 37 |
38 | the content of `status` is not sanitized against HTML. you are expected to do that yourself. in vanilla javascript, it is as simple as setting `innerText`, instead of `innerHTML`. 39 |
40 | 41 | note that some users have less data stored for them. you should only ever assume that `name`, `status` and `color` exist. 42 | 43 | ### example of how to display a status 44 | 45 | 46 |

47 | 48 | ## get reactions for a post 49 | 50 | make a GET request to `/api/reactions/:post-id` 51 | 52 | ### example response 53 | 54 | (from `https://my-ocular.jeffalo.net/api/reactions/5213429`) 55 | 56 | ```json 57 | [ 58 | { 59 | "emoji": "👍", 60 | "reactions": [ 61 | 62 | ] 63 | }, 64 | { 65 | "emoji": "👎", 66 | "reactions": [ 67 | 68 | ] 69 | }, 70 | { 71 | "emoji": "😄", 72 | "reactions": [ 73 | 74 | ] 75 | }, 76 | { 77 | "emoji": "🎉", 78 | "reactions": [ 79 | 80 | ] 81 | }, 82 | { 83 | "emoji": "😕", 84 | "reactions": [ 85 | 86 | ] 87 | }, 88 | { 89 | "emoji": "❤️", 90 | "reactions": [ 91 | 92 | ] 93 | }, 94 | { 95 | "emoji": "🚀", 96 | "reactions": [ 97 | { 98 | "_id": "60981f3ba1422d902532f0da", 99 | "post": "5213429", 100 | "user": "Jeffalo", 101 | "emoji": "🚀" 102 | } 103 | ] 104 | }, 105 | { 106 | "emoji": "👀", 107 | "reactions": [ 108 | 109 | ] 110 | } 111 | ] 112 | ``` 113 | 114 | ## prompt to set reactions for a post 115 | 116 | this is a special page on `ocular.jeffalo.net`, it works well with the my-ocular endpoint to get reactions for a post 117 | 118 | ### use it in your site 119 | 120 | 1. open a popup for `https://ocular.jeffalo.net/react/:post-id?emoji=:emoji` 121 | 2. if you're using the my-ocular endpoint to get reactions, when the pop up is closed re-fetch the reactions. 122 | 123 | ### example screenshot 124 | 125 | (from from `https://ocular.jeffalo.net/react/5213429?emoji=🚀`) 126 | 127 | ![screenshot of ocular reaction popup](/reaction-screenshot.png) 128 | 129 | ## ocular status secrets 130 | 131 | hey you! did you know you can include automatically updating elements to your ocular statuses? place one of these in your status make yourself look cool and knowledgeable! 132 | 133 | - `{joke}` for a funny joke 134 | - `{total}` for the total amount of registered ocular users 135 | - `{count}` for your post count 136 | 137 | ### bonus tip 138 | 139 | you can escape these with a backslash `\` if you don't want them to be replaced 140 | -------------------------------------------------------------------------------- /components/Loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 78 | 79 | 112 | -------------------------------------------------------------------------------- /components/Post.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 81 | 82 | 200 | 201 | -------------------------------------------------------------------------------- /components/ReactionButtons.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 125 | 126 | -------------------------------------------------------------------------------- /pages/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 171 | 172 | 188 | -------------------------------------------------------------------------------- /components/Render.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 47 | 48 | 107 | 108 | 109 | 316 | -------------------------------------------------------------------------------- /pages/dashboard/users.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 200 | 201 | 220 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 352 | -------------------------------------------------------------------------------- /pages/search.vue: -------------------------------------------------------------------------------- 1 | 159 | 160 | 220 | 323 | --------------------------------------------------------------------------------