├── app ├── .gitignore ├── .meteor │ ├── .gitignore │ ├── release │ ├── platforms │ ├── .id │ ├── .finished-upgraders │ ├── packages │ └── versions ├── packages │ ├── typescript-compiler │ │ ├── .npm │ │ │ ├── package │ │ │ │ ├── .gitignore │ │ │ │ └── README │ │ │ └── .package-garbage-yvq0nu.6ujh │ │ │ │ ├── .gitignore │ │ │ │ └── README │ │ ├── utils.js │ │ ├── README.md │ │ ├── meteor-typescript │ │ │ ├── files-source-host.js │ │ │ ├── script-snapshot.js │ │ │ ├── logger.js │ │ │ ├── utils.js │ │ │ ├── refs.js │ │ │ ├── options.js │ │ │ └── compile-service.js │ │ ├── typescript.js │ │ ├── package.js │ │ ├── logger.js │ │ └── file-utils.js │ ├── ts-decorators │ │ ├── Decorators_proxies.ts │ │ ├── README.md │ │ ├── Decorators.js │ │ ├── package.js │ │ └── ts-decorators.js │ └── vue-templates │ │ ├── VueTemplates_all.html │ │ ├── README.md │ │ ├── package.js │ │ └── vue-templates.js ├── typings │ └── index.d.ts ├── lib │ ├── server │ │ └── ACL.ts │ ├── client │ │ ├── TopBarComponent.ts │ │ ├── top-bar.html │ │ ├── list-editor.html │ │ ├── ListEditorComponent.ts │ │ └── Decorators.ts │ ├── Utilities.ts │ └── Db.ts ├── tsconfig.json ├── package.json ├── main │ ├── server │ │ └── main.ts │ └── client │ │ ├── main.html │ │ ├── main.ts │ │ └── main.css ├── imports │ ├── dashboard │ │ ├── client │ │ │ ├── DashboardComponent.ts │ │ │ └── dashboard.html │ │ └── server │ │ │ └── DashboardApi.ts │ ├── courses │ │ ├── client │ │ │ ├── courses.html │ │ │ ├── CoursesComponent.ts │ │ │ ├── CourseTreeComponent.ts │ │ │ ├── course-tree.html │ │ │ ├── course-tree.css │ │ │ └── lesson-editor.css │ │ └── server │ │ │ ├── CoursesApi.ts │ │ │ ├── WordsApi.ts │ │ │ └── SentencesApi.ts │ └── study │ │ └── client │ │ ├── study.css │ │ └── StudyComponent.ts └── public │ ├── airplane.svg │ ├── dress.svg │ ├── coffee.svg │ ├── tea.svg │ ├── train.svg │ ├── fish.svg │ ├── bus.svg │ ├── evening.svg │ ├── cat.svg │ ├── dog.svg │ ├── harbour.svg │ ├── day.svg │ ├── night.svg │ ├── apple.svg │ ├── bus-stop.svg │ ├── broken-link.svg │ ├── boy.svg │ ├── woman.svg │ ├── mouse.svg │ ├── bird.svg │ ├── man.svg │ ├── rice.svg │ ├── meat.svg │ ├── ship.svg │ ├── helicopter.svg │ ├── morning.svg │ ├── beer.svg │ ├── potato.svg │ ├── chicken.svg │ └── girl.svg ├── .gitignore ├── screenshots ├── study.png ├── study2.png ├── dashboard.png ├── course-editor.png └── lesson-editor.png ├── LICENSE.txt └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /app/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /app/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.12 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | now/bundle 2 | now/app.tar.gz 3 | -------------------------------------------------------------------------------- /app/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/.npm/.package-garbage-yvq0nu.6ujh/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /screenshots/study.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrei-markeev/finnlingo/HEAD/screenshots/study.png -------------------------------------------------------------------------------- /app/packages/ts-decorators/Decorators_proxies.ts: -------------------------------------------------------------------------------- 1 | // contents of this file will be generated automatically -------------------------------------------------------------------------------- /app/packages/vue-templates/VueTemplates_all.html: -------------------------------------------------------------------------------- 1 | // contents of this file will be generated automatically -------------------------------------------------------------------------------- /screenshots/study2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrei-markeev/finnlingo/HEAD/screenshots/study2.png -------------------------------------------------------------------------------- /screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrei-markeev/finnlingo/HEAD/screenshots/dashboard.png -------------------------------------------------------------------------------- /screenshots/course-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrei-markeev/finnlingo/HEAD/screenshots/course-editor.png -------------------------------------------------------------------------------- /screenshots/lesson-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrei-markeev/finnlingo/HEAD/screenshots/lesson-editor.png -------------------------------------------------------------------------------- /app/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module Meteor { 2 | interface User extends FinnlingoUser { } 3 | } 4 | 5 | declare var Decorators; 6 | declare var VueTemplate; 7 | 8 | interface NodeModule { 9 | dynamicImport(path: string): Promise 10 | } 11 | -------------------------------------------------------------------------------- /app/lib/server/ACL.ts: -------------------------------------------------------------------------------- 1 | export class ACL { 2 | static getUserOrThrow(methodContext) { 3 | if (methodContext.userId) 4 | return Meteor.users.findOne(methodContext.userId); 5 | throw new Meteor.Error("ACCESS_DENIED", "Access denied!"); 6 | } 7 | } -------------------------------------------------------------------------------- /app/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 63cd3w133cd2t1sdgv28 8 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/utils.js: -------------------------------------------------------------------------------- 1 | const {createHash} = Npm.require('crypto'); 2 | 3 | export function getShallowHash(ob) { 4 | const hash = createHash('sha1'); 5 | const keys = Object.keys(ob); 6 | keys.sort(); 7 | 8 | keys.forEach(key => { 9 | hash.update(key).update('' + ob[key]); 10 | }); 11 | 12 | return hash.digest('hex'); 13 | } 14 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "sourceMap": true, 9 | "importHelpers": true, 10 | "jsx": "react", 11 | "jsxFactory": "h" 12 | } 13 | } -------------------------------------------------------------------------------- /app/lib/client/TopBarComponent.ts: -------------------------------------------------------------------------------- 1 | import { vueComponent } from "./Decorators"; 2 | 3 | @vueComponent('top-bar', { 4 | props: ['backLink', 'backLinkText'] 5 | }) 6 | export class TopBarComponent { 7 | user = { study: {} }; 8 | created() { 9 | Tracker.autorun(() => { 10 | this.user = Meteor.user(); 11 | Meteor.subscribe('userData'); 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /app/lib/client/top-bar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/README.md: -------------------------------------------------------------------------------- 1 | ## TypeScript compiler for Meteor [![Build Status](https://travis-ci.org/barbatus/typescript-compiler.svg?branch=master)](https://travis-ci.org/barbatus/typescript-compiler) 2 | 3 | Exports two symbols: 4 | - `TypeScriptCompiler` - a compiler to be registered using `registerBuildPlugin` 5 | to compile TypeScript files. 6 | 7 | - `TypeScript` - an object with `compile` method. 8 | Use `TypeScript.compile(source, options)` to compile with preset options. 9 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/.npm/.package-garbage-yvq0nu.6ujh/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run", 6 | "bundle-now": "meteor build --architecture=os.linux.x86_64 ../now/ && cd ../now && node split.js -b 4m app.tar.gz bundle/finnlingo." 7 | }, 8 | "dependencies": { 9 | "@babel/runtime": "^7.12.5", 10 | "@types/meteor": "^1.4.64", 11 | "core-js": "^2.5.7", 12 | "meteor-node-stubs": "~0.2.4", 13 | "tslib": "^2.1.0", 14 | "vue": "^2.6.12", 15 | "vue-router": "^3.4.9" 16 | }, 17 | "devDependencies": { 18 | "node-split": "^1.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/packages/ts-decorators/README.md: -------------------------------------------------------------------------------- 1 | Typescript Decorators 2 | ===================== 3 | 4 | On server: 5 | 6 | class MyPage { 7 | @Decorators.publish 8 | public static Subscribe() { 9 | return MyCollection.find({}); 10 | } 11 | 12 | @Decorators.method 13 | public static DoSomething(param1, param2, callback) { 14 | // do something 15 | } 16 | } 17 | 18 | And then on client: 19 | 20 | MyPage.Subscribe(); 21 | MyPage.DoSomething(1, 2, function(err, res) { 22 | if (err) 23 | alert("ERROR!"); 24 | }) 25 | 26 | -------------------------------------------------------------------------------- /app/main/server/main.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConfiguration } from "meteor/service-configuration"; 2 | 3 | Meteor.startup(() => { 4 | 5 | ServiceConfiguration.configurations.upsert( 6 | { service: 'facebook' }, 7 | { 8 | $set: { 9 | loginStyle: "redirect", 10 | appId: process.env.FB_APP_ID, 11 | secret: process.env.FB_APP_SECRET 12 | } 13 | } 14 | ); 15 | 16 | Meteor.publish('userData', function() { 17 | return Meteor.users.find({ _id: this.userId }, { fields: { study: 1 } }); 18 | }); 19 | 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /app/packages/vue-templates/README.md: -------------------------------------------------------------------------------- 1 | Vue templates 2 | ============= 3 | 4 | Light-weight replacement for **blaze-html-templates** for working with Vue.js templates. 5 | Vue templates can be defined similarly to blaze templates using **template** tags, e.g.: 6 | 7 | 10 | 11 | Also you can as usually define **body** and **head** tags, they will be merged together into the static html file. 12 | 13 | Template contents are then accessible via VueTemplate variable: 14 | 15 | Vue.component("myComponent", { template: VueTemplate["myVueTemplate"] }); 16 | 17 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/files-source-host.js: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import _ from "underscore"; 3 | 4 | const ROOTED = /^(\/|\\)/; 5 | 6 | class SourceHost { 7 | setSource(fileSource) { 8 | this.fileSource = fileSource; 9 | } 10 | 11 | get(filePath) { 12 | if (this.fileSource) { 13 | const source = this.fileSource(filePath); 14 | if (_.isString(source)) return source; 15 | } 16 | 17 | return null; 18 | } 19 | 20 | normalizePath(filePath) { 21 | if (!filePath) return null; 22 | 23 | return filePath.replace(ROOTED, ''); 24 | } 25 | } 26 | 27 | module.exports = new SourceHost(); 28 | -------------------------------------------------------------------------------- /app/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | 1.4.0-remove-old-dev-bundle-link 15 | 1.4.1-add-shell-server-package 16 | 1.4.3-split-account-service-packages 17 | 1.5-add-dynamic-import-package 18 | 1.7-split-underscore-from-meteor-base 19 | 1.8.3-split-jquery-from-blaze 20 | -------------------------------------------------------------------------------- /app/main/client/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Finnlingo 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/packages/vue-templates/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'vue-templates', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: 'Compile vue.js templates.', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.registerBuildPlugin({ 14 | name: "compileVueTemplates", 15 | sources: ['vue-templates.js'] 16 | }); 17 | 18 | Package.onUse(function(api) { 19 | api.versionsFrom('1.4.1'); 20 | api.use('isobuild:compiler-plugin@1.0.0'); 21 | api.addFiles('VueTemplates_all.html', ['web.browser'], { bare: true }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/packages/ts-decorators/Decorators.js: -------------------------------------------------------------------------------- 1 | Decorators = this.Decorators || {}; 2 | Decorators.publish = function (target, propertyKey, descriptor) { 3 | var originalMethod = descriptor.value; 4 | var publicationName = target.toString().match("function ([A-Za-z0-9_]+)")[1] + "." + propertyKey; 5 | 6 | Meteor.publish(publicationName, originalMethod); 7 | 8 | return descriptor; 9 | } 10 | 11 | Decorators.method = function (target, propertyKey, descriptor) { 12 | var originalMethod = descriptor.value; 13 | var methodName = target.toString().match("function ([A-Za-z0-9_]+)")[1] + "." + propertyKey; 14 | 15 | var methodsObj = {}; 16 | methodsObj[methodName] = originalMethod; 17 | Meteor.methods(methodsObj); 18 | 19 | return descriptor; 20 | } 21 | 22 | this.Decorators = Decorators; -------------------------------------------------------------------------------- /app/packages/typescript-compiler/typescript.js: -------------------------------------------------------------------------------- 1 | import * as meteorTS from './meteor-typescript'; 2 | 3 | TypeScript = { 4 | validateOptions(options) { 5 | if (! options) return; 6 | 7 | meteorTS.validateAndConvertOptions(options); 8 | }, 9 | 10 | // Extra options are the same compiler options 11 | // but passed in the compiler constructor. 12 | validateExtraOptions(options) { 13 | if (! options) return; 14 | 15 | meteorTS.validateAndConvertOptions({ 16 | compilerOptions: options 17 | }); 18 | }, 19 | 20 | getDefaultOptions: meteorTS.getDefaultOptions, 21 | 22 | compile(source, options) { 23 | options = options || meteorTS.getDefaultOptions(); 24 | return meteorTS.compile(source, options); 25 | }, 26 | 27 | setCacheDir(cacheDir) { 28 | meteorTS.setCacheDir(cacheDir); 29 | }, 30 | 31 | isDeclarationFile(filePath) { 32 | return /^.*\.d\.ts$/.test(filePath); 33 | }, 34 | 35 | removeTsExt(path) { 36 | return path && path.replace(/(\.tsx|\.ts)$/g, ''); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'barbatus:typescript-compiler', 3 | version: '0.11.0', 4 | summary: 'TypeScript Compiler for Meteor', 5 | git: 'https://github.com/barbatus/typescript-compiler', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Npm.depends({ 10 | 'async': '2.5.0', 11 | 'colors': '1.1.2', 12 | 'chalk': '2.4.1', 13 | 'random-js': '1.0.8', 14 | 'object-sizeof': '1.3.0', 15 | 'underscore': '1.9.1', 16 | 'diff': '2.2.2', 17 | 'lru-cache': '4.1.1', 18 | '@babel/core': '7.5.5', 19 | 'typescript': '3.5.2' 20 | }); 21 | 22 | Package.onUse(function(api) { 23 | api.versionsFrom('1.4.1'); 24 | 25 | api.use([ 26 | 'ecmascript@0.10.8', 27 | 'check@1.0.5', 28 | 'underscore@1.0.4', 29 | ], 'server'); 30 | 31 | api.addFiles([ 32 | 'logger.js', 33 | 'file-utils.js', 34 | 'typescript-compiler.js', 35 | 'typescript.js', 36 | 'utils.js', 37 | ], 'server'); 38 | 39 | api.export([ 40 | 'TypeScript', 41 | 'TypeScriptCompiler', 42 | ], 'server'); 43 | }); 44 | -------------------------------------------------------------------------------- /app/packages/ts-decorators/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'ts-decorators', 3 | version: '0.0.4', 4 | // Brief, one-line summary of the package. 5 | summary: 'Allows using TypeScript decorators @Decorators.method and @Decorators.publish.', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.registerBuildPlugin({ 14 | name: "tsDecorators", 15 | use: ['barbatus:typescript-compiler'], 16 | sources: ['ts-decorators.js'] 17 | }); 18 | 19 | Package.onUse(function(api) { 20 | api.versionsFrom('1.4.1'); 21 | api.use('isobuild:compiler-plugin@1.0.0'); 22 | api.use('barbatus:typescript-compiler@0.11.0'); 23 | api.addFiles('Decorators.js', 'server'); 24 | api.addFiles('Decorators_proxies.ts', ['web.browser'], { bare: true }); 25 | 26 | api.imply("modules@0.11.6"); 27 | api.imply("barbatus:typescript-runtime@1.1.0"); 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrei Markeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base@1.4.0 # Packages every Meteor app needs to have 8 | mobile-experience@1.1.0 # Packages for a great mobile UX 9 | mongo@1.10.1 # The database Meteor supports right now 10 | reactive-var@1.0.11 # Reactive variable for tracker 11 | tracker@1.2.0 # Meteor's client-side reactive programming library 12 | 13 | standard-minifier-css@1.7.1 # CSS minifier run for production mode 14 | standard-minifier-js@2.6.0 # JS minifier run for production mode 15 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers 16 | shell-server@0.5.0 # Server-side component of the `meteor shell` command 17 | vue-templates 18 | ts-decorators 19 | accounts-base@1.7.1 20 | http@1.4.2 21 | accounts-facebook@1.3.2 22 | service-configuration@1.0.11 23 | underscore@1.0.10 24 | barbatus:typescript-compiler@0.11.0 25 | -------------------------------------------------------------------------------- /app/imports/dashboard/client/DashboardComponent.ts: -------------------------------------------------------------------------------- 1 | import VueRouter, { Route } from "vue-router"; 2 | import { vueComponent } from "../../../lib/client/Decorators"; 3 | import { DashboardApi } from "../server/DashboardApi"; 4 | 5 | @vueComponent('dashboard') 6 | export class DashboardComponent { 7 | 8 | $route: Route; 9 | $router: VueRouter; 10 | 11 | course: Course = null; 12 | loggingIn: boolean = true; 13 | user: FinnlingoUser = null; 14 | todayLeaders = []; 15 | allTimeLeaders = []; 16 | showSideBar = false; 17 | windowWidth = 1200; 18 | 19 | created() { 20 | this.windowWidth = document.documentElement.clientWidth; 21 | window.addEventListener('resize', e => { 22 | this.windowWidth = document.documentElement.clientWidth; 23 | }); 24 | this.getPageData(); 25 | Tracker.autorun(() => { 26 | this.loggingIn = Meteor.loggingIn(); 27 | this.user = Meteor.user(); 28 | }) 29 | } 30 | 31 | async getPageData() { 32 | const res = await DashboardApi.getDashboardPageData(); 33 | this.course = res.course; 34 | this.todayLeaders = res.todayLeaders; 35 | this.allTimeLeaders = res.allTimeLeaders; 36 | } 37 | } -------------------------------------------------------------------------------- /app/lib/client/list-editor.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/lib/Utilities.ts: -------------------------------------------------------------------------------- 1 | export class Utilities { 2 | static sentenceToWords(s: string) { 3 | var sentence = s.split(/[,\.-\?!:\s"]+/).join(' ').replace(/^\s+/,'').replace(/\s+$/,'').toLowerCase(); 4 | sentence = sentence.replace(/i'm/g,"i am").replace(/it's/g,"it is"); 5 | sentence = sentence.replace(/they're/g,"they are").replace(/we're/g,"we are").replace(/you're/g,"you are"); 6 | sentence = sentence.replace(/don't/g,"do not").replace(/doesn't/g,"does not").replace(/didn't/g,"did not"); 7 | sentence = sentence.replace(/\b(a|an|the) ([a-zA-Z']+)/g, "$2"); 8 | return sentence.split(' '); 9 | } 10 | 11 | static getSentenceTokens(text) { 12 | var tokens = []; 13 | var delimiterRegex = /[,\.-\?!:\s"]/; 14 | for (let i = 0; i < text.length; i++) { 15 | let l = tokens.length; 16 | if (l && !delimiterRegex.test(tokens[l-1]) && !delimiterRegex.test(text[i])) 17 | tokens[l-1] += text[i]; 18 | else 19 | tokens.push(text[i]); 20 | } 21 | return tokens; 22 | } 23 | 24 | static getPictureId(text) { 25 | return text.replace(/^(the\s+|a\s+|an\s+)/,'').replace(/ /g,'-'); 26 | } 27 | } 28 | (this as any).Utilities = Utilities; 29 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/logger.js: -------------------------------------------------------------------------------- 1 | const util = Npm.require('util'); 2 | 3 | class Logger_ { 4 | constructor() { 5 | this.llevel = process.env.TYPESCRIPT_LOG; 6 | } 7 | 8 | newProfiler(name) { 9 | let profiler = new Profiler(name); 10 | if (this.isProfile) profiler.start(); 11 | return profiler; 12 | } 13 | 14 | get isDebug() { 15 | return this.llevel >= 2; 16 | } 17 | 18 | get isProfile() { 19 | return this.llevel >= 3; 20 | } 21 | 22 | get isAssert() { 23 | return this.llevel >= 4; 24 | } 25 | 26 | log(msg, ...args) { 27 | if (this.llevel >= 1) { 28 | console.log.apply(null, [msg].concat(args)); 29 | } 30 | } 31 | 32 | debug(msg, ...args) { 33 | if (this.isDebug) { 34 | this.log.apply(this, msg, args); 35 | } 36 | } 37 | 38 | assert(msg, ...args) { 39 | if (this.isAssert) { 40 | this.log.apply(this, msg, args); 41 | } 42 | } 43 | }; 44 | 45 | Logger = new Logger_(); 46 | 47 | class Profiler { 48 | constructor(name) { 49 | this.name = name; 50 | } 51 | 52 | start() { 53 | console.log('%s started', this.name); 54 | console.time(util.format('%s time', this.name)); 55 | this._started = true; 56 | } 57 | 58 | end() { 59 | if (this._started) { 60 | console.timeEnd(util.format('%s time', this.name)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/script-snapshot.js: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | import * as jsdiff from "diff"; 3 | 4 | import logger from "./logger"; 5 | 6 | export default class StringScriptSnapshot { 7 | constructor(text) { 8 | this.text = text; 9 | } 10 | 11 | getText(start, end) { 12 | return this.text.substring(start, end); 13 | } 14 | 15 | getLength() { 16 | return this.text.length; 17 | } 18 | 19 | getChangeRange(oldSnapshot) { 20 | if (!oldSnapshot) return undefined; 21 | 22 | const diffs = jsdiff.diffChars(oldSnapshot.text, this.text); 23 | if (diffs.length) { 24 | let ind = 0; 25 | let changes = []; 26 | for (let i = 0; i < diffs.length; i++) { 27 | const diff = diffs[i]; 28 | 29 | if (diff.added) { 30 | changes.push(ts.createTextChangeRange( 31 | ts.createTextSpan(ind, 0), diff.count)); 32 | ind += diff.count; 33 | continue; 34 | } 35 | 36 | if (diff.removed) { 37 | changes.push(ts.createTextChangeRange( 38 | ts.createTextSpan(ind, diff.count), 0)); 39 | continue; 40 | } 41 | 42 | ind += diff.count; 43 | } 44 | 45 | changes = ts.collapseTextChangeRangesAcrossMultipleVersions(changes); 46 | logger.assert("accumulated file changes %j", changes); 47 | 48 | return changes; 49 | } 50 | 51 | return ts.createTextChangeRange(ts.createTextSpan(0, 0), 0); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/imports/dashboard/server/DashboardApi.ts: -------------------------------------------------------------------------------- 1 | import { ACL } from "../../../lib/server/ACL"; 2 | 3 | export class DashboardApi { 4 | @Decorators.method 5 | static getDashboardPageData(callback?) { 6 | var user = ACL.getUserOrThrow(this); 7 | var today = new Date(); 8 | today.setHours(0, 0, 0); 9 | var todayLeaders = Meteor.users.find( 10 | { "study.lastDateStudied": { $gte: today.getTime() } }, 11 | { sort: { "study.lastDateXP": -1 }, limit: 10, fields: { "profile.name": 1, "study.lastDateXP": 1, "services.facebook.id": 1 } }) 12 | .fetch() 13 | .map(tl => ({ 14 | name: tl.profile.name, 15 | xp: tl.study.lastDateXP, 16 | avatarUrl: "http://graph.facebook.com/" + tl.services.facebook.id + "/picture" 17 | })); 18 | var allTimeLeaders = Meteor.users.find( 19 | { }, 20 | { sort: { "study.xp": -1 }, limit: 10, fields: { "profile.name": 1, "study.xp": 1, "services.facebook.id": 1 } }) 21 | .fetch() 22 | .map(tl => ({ 23 | name: tl.profile.name, 24 | xp: tl.study.xp, 25 | avatarUrl: "http://graph.facebook.com/" + tl.services.facebook.id + "/picture" 26 | })); 27 | return { 28 | course: Courses.findOne(user.selectedCourseId), 29 | todayLeaders: todayLeaders, 30 | allTimeLeaders: allTimeLeaders 31 | }; 32 | } 33 | } -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/logger.js: -------------------------------------------------------------------------------- 1 | var util = require("util"); 2 | 3 | function Logger() { 4 | this.prefix = "[meteor-typescript]: "; 5 | this.llevel = process.env.TYPESCRIPT_LOG; 6 | } 7 | 8 | var LP = Logger.prototype; 9 | 10 | LP.debug = function(format, ...args) { 11 | if (this.isDebug()) { 12 | var msg = args.length ? util.format(format, ...args) : format; 13 | console.log(this.prefix + msg); 14 | } 15 | }; 16 | 17 | LP.assert = function(format, ...args) { 18 | if (this.isAssert()) { 19 | var msg = args.length ? util.format(format, ...args) : format; 20 | console.log(this.prefix + msg); 21 | } 22 | }; 23 | 24 | LP.isDebug = function() { 25 | return this.llevel >= 2; 26 | }; 27 | 28 | LP.isProfile = function() { 29 | return this.llevel >= 3; 30 | }; 31 | 32 | LP.isAssert = function() { 33 | return this.llevel >= 4; 34 | }; 35 | 36 | LP.newProfiler = function(name) { 37 | var fullName = util.format("%s%s", this.prefix, name); 38 | var profiler = new Profiler(fullName); 39 | if (this.isProfile()) profiler.start(); 40 | return profiler; 41 | }; 42 | 43 | 44 | function Profiler(name) { 45 | this.name = name; 46 | } 47 | 48 | var PP = Profiler.prototype; 49 | 50 | PP.start = function() { 51 | console.log("%s started", this.name); 52 | console.time(util.format("%s time", this.name)); 53 | this._started = true; 54 | }; 55 | 56 | PP.end = function() { 57 | if (this._started) { 58 | console.timeEnd(util.format("%s time", this.name)); 59 | } 60 | }; 61 | 62 | export default new Logger(); 63 | -------------------------------------------------------------------------------- /app/lib/client/ListEditorComponent.ts: -------------------------------------------------------------------------------- 1 | import { vueComponent } from "./Decorators"; 2 | 3 | @vueComponent('list-editor', { 4 | props: ['items', 'canAdd', 'canEdit', 'canRemove', 'newItemText', 'itemClass'] 5 | }) 6 | export class ListEditorComponent { 7 | $emit: Function; 8 | $nextTick: Function; 9 | $el: HTMLElement; 10 | canEdit: string | Function; 11 | 12 | editingInline = null; 13 | 14 | mounted() { 15 | this.editingInline = null; 16 | } 17 | 18 | getItemText(item) { 19 | if (this.canEdit && typeof this.canEdit === 'string' && this.canEdit !== 'true') 20 | return item[this.canEdit]; 21 | else 22 | return item.name; 23 | } 24 | 25 | selectItem(item) { 26 | this.$emit('select', item); 27 | } 28 | 29 | startInlineEditing(item) { 30 | this.$emit('edit', item); 31 | this.editingInline = null; 32 | } 33 | 34 | editItem(item) { 35 | if (this.canEdit && this.canEdit !== 'true' && typeof this.canEdit === 'string') { 36 | this.editingInline = item; 37 | this.$nextTick(() => { 38 | (this.$el.querySelector('input[type="text"]')).focus(); 39 | }); 40 | } else 41 | this.$emit('edit', item); 42 | } 43 | 44 | endInlineEditing(item) { 45 | this.$emit('edit', item); 46 | this.editingInline = null; 47 | } 48 | 49 | removeItem(item) { 50 | if (!confirm("Are you sure want to delete this item? Action cannot be undone!")) 51 | return; 52 | this.$emit('remove', item); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/public/airplane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/imports/dashboard/client/dashboard.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.7.1 2 | accounts-facebook@1.3.2 3 | accounts-oauth@1.2.0 4 | allow-deny@1.1.0 5 | autoupdate@1.6.0 6 | babel-compiler@7.5.5 7 | babel-runtime@1.5.0 8 | barbatus:typescript-compiler@0.11.0 9 | barbatus:typescript-runtime@1.1.0 10 | base64@1.0.12 11 | binary-heap@1.0.11 12 | boilerplate-generator@1.7.1 13 | callback-hook@1.3.0 14 | check@1.3.1 15 | ddp@1.4.0 16 | ddp-client@2.3.3 17 | ddp-common@1.4.0 18 | ddp-rate-limiter@1.0.9 19 | ddp-server@2.3.2 20 | diff-sequence@1.1.1 21 | dynamic-import@0.5.5 22 | ecmascript@0.14.4 23 | ecmascript-runtime@0.7.0 24 | ecmascript-runtime-client@0.11.0 25 | ecmascript-runtime-server@0.10.0 26 | ejson@1.1.1 27 | es5-shim@4.8.0 28 | facebook-oauth@1.7.4 29 | fetch@0.1.1 30 | geojson-utils@1.0.10 31 | hot-code-push@1.0.4 32 | http@1.4.2 33 | id-map@1.1.0 34 | inter-process-messaging@0.1.1 35 | launch-screen@1.2.0 36 | livedata@1.0.18 37 | localstorage@1.2.0 38 | logging@1.1.20 39 | meteor@1.9.3 40 | meteor-base@1.4.0 41 | minifier-css@1.5.3 42 | minifier-js@2.6.0 43 | minimongo@1.6.1 44 | mobile-experience@1.1.0 45 | mobile-status-bar@1.1.0 46 | modern-browsers@0.1.5 47 | modules@0.15.0 48 | modules-runtime@0.12.0 49 | mongo@1.10.1 50 | mongo-decimal@0.1.2 51 | mongo-dev-server@1.1.0 52 | mongo-id@1.0.7 53 | npm-mongo@3.8.1 54 | oauth@1.3.2 55 | oauth2@1.3.0 56 | ordered-dict@1.1.0 57 | promise@0.11.2 58 | random@1.2.0 59 | rate-limit@1.0.9 60 | reactive-var@1.0.11 61 | reload@1.3.1 62 | retry@1.1.0 63 | routepolicy@1.1.0 64 | service-configuration@1.0.11 65 | shell-server@0.5.0 66 | socket-stream-client@0.3.1 67 | standard-minifier-css@1.7.1 68 | standard-minifier-js@2.6.0 69 | tracker@1.2.0 70 | ts-decorators@0.0.4 71 | underscore@1.0.10 72 | url@1.3.1 73 | vue-templates@0.0.1 74 | webapp@1.9.1 75 | webapp-hashing@1.0.9 76 | -------------------------------------------------------------------------------- /app/public/dress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/imports/courses/client/courses.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/public/coffee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/public/tea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finnlingo 2 | 3 | Duolingo-like application for learning Finnish. 4 | 5 | The app was created because Finnish was not being accepted to Duolingo Incubator for a very long time despite of having a lot of course contributor volunteers. 6 | 7 | See more details in the "Finnish on Duolingo" Facebook group: https://www.facebook.com/groups/finnishonduolingo/ 8 | 9 | **Update**: at last, Finnish is in the Incubator! :) 10 | 11 | Working version of the app with some test content can be found here: https://finnlingo.herokuapp.com 12 | You can create your own courses or use existing courses in the app. It might be possible to create courses for other languages than Finnish too. 13 | 14 | ## Contributing 15 | 16 | Development can be done on Mac, Windows or Linux. 17 | 18 | App is created with Vue.js and Meteor.js using TypeScript. 19 | Application code is found under /app/components and /app/lib. Database structure described in /app/lib/Db.ts. 20 | 21 | Development setup: 22 | 23 | 1. [Install Meteor](https://www.meteor.com/install) 24 | 2. Fork 25 | 3. `git clone https://github.com//finnlingo.git` 26 | 4. `meteor npm install` 27 | 5. Create a FB app (for authentication) at https://developers.facebook.com and put app id and secret into /app/components/main/server/main.ts. 28 | 6. `meteor` 29 | 7. If everything is fine, the app should be accessible at http://localhost:3000 30 | 31 | Note: Do not commit your FB app id and secret! You can use `git update-index --assume-unchanged /app/components/main/server/main.ts` to achieve this. 32 | 33 | ## Screenshots 34 | 35 | ![Dashboard](https://raw.github.com/andrei-markeev/finnlingo/master/screenshots/dashboard.png) 36 | 37 | ![Lesson practice](https://raw.github.com/andrei-markeev/finnlingo/master/screenshots/study.png) 38 | 39 | ![Lesson completed](https://raw.github.com/andrei-markeev/finnlingo/master/screenshots/study2.png) 40 | 41 | ![Course editor](https://raw.github.com/andrei-markeev/finnlingo/master/screenshots/course-editor.png) 42 | 43 | ![Lesson editor](https://raw.github.com/andrei-markeev/finnlingo/master/screenshots/lesson-editor.png) 44 | 45 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/file-utils.js: -------------------------------------------------------------------------------- 1 | const colors = Npm.require('colors'); 2 | 3 | export function isBare(inputFile) { 4 | const fileOptions = inputFile.getFileOptions(); 5 | return fileOptions && fileOptions.bare; 6 | } 7 | 8 | // Gets root app tsconfig. 9 | export function isMainConfig(inputFile) { 10 | if (! isWeb(inputFile)) return false; 11 | 12 | const filePath = inputFile.getPathInPackage(); 13 | return /^tsconfig\.json$/.test(filePath); 14 | } 15 | 16 | export function isConfig(inputFile) { 17 | const filePath = inputFile.getPathInPackage(); 18 | return /tsconfig\.json$/.test(filePath); 19 | } 20 | 21 | // Gets server tsconfig. 22 | export function isServerConfig(inputFile) { 23 | if (isWeb(inputFile)) return false; 24 | 25 | const filePath = inputFile.getPathInPackage(); 26 | return /^server\/tsconfig\.json$/.test(filePath); 27 | } 28 | 29 | // Checks if it's .d.ts-file. 30 | export function isDeclaration(inputFile) { 31 | return TypeScript.isDeclarationFile(inputFile.getBasename()); 32 | } 33 | 34 | export function isWeb(inputFile) { 35 | const arch = inputFile.getArch(); 36 | return /^web/.test(arch); 37 | } 38 | 39 | // Gets path with package prefix if any. 40 | export function getExtendedPath(inputFile) { 41 | let packageName = inputFile.getPackageName(); 42 | packageName = packageName ? 43 | (packageName.replace(':', '_') + '/') : ''; 44 | const inputFilePath = inputFile.getPathInPackage(); 45 | return packageName + inputFilePath; 46 | } 47 | 48 | export function getES6ModuleName(inputFile) { 49 | const extended = getExtendedPath(inputFile); 50 | return TypeScript.removeTsExt(extended); 51 | } 52 | 53 | export const WarnMixin = { 54 | warn(error) { 55 | console.log(`${error.sourcePath} (${error.line}, ${error.column}): ${error.message}`); 56 | }, 57 | logError(error) { 58 | console.log(colors.red( 59 | `${error.sourcePath} (${error.line}, ${error.column}): ${error.message}`)); 60 | } 61 | } 62 | 63 | export function extendFiles(inputFiles, fileMixin) { 64 | inputFiles.forEach(inputFile => _.defaults(inputFile, fileMixin)); 65 | } 66 | -------------------------------------------------------------------------------- /app/imports/courses/server/CoursesApi.ts: -------------------------------------------------------------------------------- 1 | import { SentenceTestType } from "../../../lib/Db"; 2 | import { ACL } from "../../../lib/server/ACL"; 3 | 4 | export class CoursesApi { 5 | @Decorators.publish 6 | static subscribeToCourses(): Mongo.Cursor { 7 | var user = ACL.getUserOrThrow(this); 8 | return Courses.find(); 9 | } 10 | 11 | @Decorators.method 12 | static getSentencesCount(courseId, callback?) { 13 | let tree = Courses.findOne(courseId, { fields: { tree: 1 }}).tree; 14 | let lessonIds = []; 15 | for (let row of tree) 16 | lessonIds = lessonIds.concat(row.lessons.map(l => l.id)); 17 | 18 | return Sentences.find({ lessonId: { $in: lessonIds }, testType: { $ne: SentenceTestType.Notes } }).count(); 19 | } 20 | @Decorators.method 21 | static getAvatarUrl(userId, callback?) { 22 | let user = Meteor.users.findOne(userId, { fields: { "services.facebook.id": 1} }); 23 | return "http://graph.facebook.com/" + user.services.facebook.id + "/picture"; 24 | } 25 | 26 | @Decorators.method 27 | static addCourse(name: string, callback?) { 28 | var user = ACL.getUserOrThrow(this); 29 | Courses.insert({ 30 | name: name, 31 | tree: [], 32 | admin_ids: [user._id] 33 | }); 34 | } 35 | 36 | @Decorators.method 37 | static updateCourse(courseModel, callback?) { 38 | var user = ACL.getUserOrThrow(this); 39 | Courses.update( 40 | { _id: courseModel._id, admin_ids: user._id }, 41 | { $set: { name: courseModel.name, tree: courseModel.tree } } 42 | ); 43 | } 44 | 45 | @Decorators.method 46 | static selectCourse(courseId, callback?) { 47 | var user = ACL.getUserOrThrow(this); 48 | Meteor.users.update(user._id, { 49 | $set: { selectedCourseId: courseId } 50 | }); 51 | } 52 | 53 | @Decorators.method 54 | static removeCourse(course, callback?) { 55 | var user = ACL.getUserOrThrow(this); 56 | Courses.remove( 57 | { _id: course._id, admin_ids: user._id } 58 | ); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/imports/courses/client/CoursesComponent.ts: -------------------------------------------------------------------------------- 1 | import VueRouter, { Route } from "vue-router"; 2 | import { vueComponent } from "../../../lib/client/Decorators"; 3 | import { CoursesApi } from "../server/CoursesApi"; 4 | 5 | @vueComponent("courses") 6 | export class CoursesComponent 7 | { 8 | $route: Route; 9 | $router: VueRouter; 10 | $set: Function; 11 | 12 | courses: Course[] = null; 13 | course: Course = null; 14 | loggingIn: boolean = true; 15 | user: FinnlingoUser = null; 16 | avatarUrls: { [key: string]: string } = {}; 17 | sentencesCount: { [key: string]: number } = {}; 18 | 19 | created() { 20 | CoursesApi.subscribeToCourses(); 21 | Tracker.autorun(() => { 22 | this.loggingIn = Meteor.loggingIn(); 23 | this.user = Meteor.user(); 24 | this.courses = Courses.find().fetch(); 25 | if (this.$route.params.id) 26 | this.course = this.courses.filter(c => c._id == this.$route.params.id)[0]; 27 | 28 | this.processCourses(); 29 | }) 30 | } 31 | 32 | async processCourses() { 33 | for (let c of this.courses) { 34 | const count = await CoursesApi.getSentencesCount(c._id) 35 | this.sentencesCount[c._id] = count; 36 | this.courses = this.courses.sort((a, b) => this.sentencesCount[b._id] - this.sentencesCount[a._id]); 37 | for (let id of c.admin_ids) { 38 | if (!this.avatarUrls[id]) { 39 | const url = await CoursesApi.getAvatarUrl(id); 40 | this.$set(this.avatarUrls, id, url); 41 | } 42 | } 43 | } 44 | } 45 | async selectCourse(course) { 46 | try { 47 | await CoursesApi.selectCourse(course._id); 48 | this.$router.push("/"); 49 | } catch(err) { 50 | alert("Error occured: " + err.toString()); 51 | } 52 | } 53 | 54 | canEdit(course) { 55 | return course.admin_ids.indexOf(this.user._id) > -1; 56 | } 57 | 58 | editCourse(course) { 59 | this.course = this.courses.filter(c => c._id == course._id)[0]; 60 | this.$router.push("/courses/" + course._id); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/main/client/main.ts: -------------------------------------------------------------------------------- 1 | import VueRouter from "vue-router"; 2 | import Vue from "vue/dist/vue.min.js"; 3 | import { CourseTreeComponent } from "../../imports/courses/client/CourseTreeComponent"; 4 | import { ListEditorComponent } from "../../lib/client/ListEditorComponent"; 5 | import { TopBarComponent } from "../../lib/client/TopBarComponent"; 6 | 7 | Meteor.startup(() => { 8 | 9 | Vue.use(VueRouter); 10 | 11 | const dashboardComponent = () => module.dynamicImport('../../imports/dashboard/client/DashboardComponent').then(c => new c.DashboardComponent()); 12 | const studyComponent = () => module.dynamicImport('../../imports/study/client/StudyComponent').then(c => new c.StudyComponent()); 13 | const coursesComponent = () => module.dynamicImport('../../imports/courses/client/CoursesComponent').then(c => new c.CoursesComponent()); 14 | const lessonEditorComponent = () => module.dynamicImport('../../imports/courses/client/LessonEditorComponent').then(c => new c.LessonEditorComponent()); 15 | 16 | new ListEditorComponent(); 17 | new TopBarComponent(); 18 | new CourseTreeComponent(); 19 | 20 | var router = new VueRouter({ 21 | mode: 'history', 22 | routes: [ 23 | { path: '/', component: dashboardComponent }, 24 | { path: '/login', component: { template: VueTemplate['login'] } }, 25 | { path: '/study/:courseid/lessons/:lessonid', component: studyComponent }, 26 | { path: '/courses', component: coursesComponent }, 27 | { path: '/courses/:id', component: coursesComponent }, 28 | { path: '/courses/:id/lessons/:lessonid', component: lessonEditorComponent } 29 | ] 30 | }); 31 | 32 | router.beforeEach((to, from, next) => { 33 | Tracker.autorun(() => { 34 | if (!Meteor.loggingIn()) { 35 | if (Meteor.user() && to.path == '/login') 36 | next('/'); 37 | else if (Meteor.user()) 38 | next(); 39 | else if (to.path == '/login') 40 | next(); 41 | else 42 | next('/login'); 43 | } 44 | }); 45 | }); 46 | 47 | 48 | new Vue({ 49 | el: "#app", 50 | router: router 51 | }); 52 | 53 | Meteor.subscribe('userData'); 54 | 55 | }); -------------------------------------------------------------------------------- /app/public/train.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 14 | 15 | 17 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/utils.js: -------------------------------------------------------------------------------- 1 | // Copied from https://github.com/meteor/babel/blob/master/util.js 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import { createHash } from "crypto"; 6 | import assert from "assert"; 7 | import _ from "underscore"; 8 | 9 | exports.mkdirp = function mkdirp(dir) { 10 | if (! fs.existsSync(dir)) { 11 | var parentDir = path.dirname(dir); 12 | if (parentDir !== dir) { 13 | mkdirp(parentDir); 14 | } 15 | 16 | try { 17 | fs.mkdirSync(dir); 18 | } catch (error) { 19 | if (error.code !== "EEXIST") { 20 | throw error; 21 | } 22 | } 23 | } 24 | 25 | return dir; 26 | }; 27 | 28 | // Borrowed from another MIT-licensed project that benjamn wrote: 29 | // https://github.com/reactjs/commoner/blob/235d54a12c/lib/util.js#L136-L168 30 | function deepHash(val) { 31 | var hash = createHash("sha1"); 32 | var type = typeof val; 33 | 34 | if (val === null) { 35 | type = "null"; 36 | } 37 | 38 | switch (type) { 39 | case "object": 40 | var keys = Object.keys(val); 41 | 42 | // Array keys will already be sorted. 43 | if (! Array.isArray(val)) { 44 | keys.sort(); 45 | } 46 | 47 | keys.forEach(function(key) { 48 | if (typeof val[key] === "function") { 49 | // Silently ignore nested methods, but nevertheless complain below 50 | // if the root value is a function. 51 | return; 52 | } 53 | 54 | hash.update(key + "\0").update(deepHash(val[key])); 55 | }); 56 | 57 | break; 58 | 59 | case "function": 60 | assert.ok(false, "cannot hash function objects"); 61 | break; 62 | 63 | default: 64 | hash.update("" + val); 65 | break; 66 | } 67 | 68 | return hash.digest("hex"); 69 | } 70 | 71 | exports.deepHash = function(val) { 72 | var argc = arguments.length; 73 | if (argc === 1) { 74 | return deepHash(val); 75 | } 76 | 77 | var args = new Array(argc); 78 | for (var i = 0; i < argc; ++i) { 79 | args[i] = arguments[i]; 80 | } 81 | 82 | return deepHash(args); 83 | }; 84 | 85 | exports.assertProps = function(obj, props) { 86 | assert.ok(obj); 87 | assert.ok(props); 88 | 89 | var len = props.length; 90 | for (var i = 0; i < len; i++) { 91 | assert.ok(_.has(obj, props[i]), `Prop ${props[i]} not defined`); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /app/public/fish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 11 | 12 | 15 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/public/bus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/public/evening.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 15 | 16 | 17 | 19 | 21 | 23 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/public/cat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 13 | 17 | 18 | 20 | 22 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/refs.js: -------------------------------------------------------------------------------- 1 | import logger from "./logger"; 2 | 3 | export const RefsChangeType = { 4 | NONE: 0, 5 | FILES: 1, 6 | MODULES: 2, 7 | TYPINGS: 3, 8 | }; 9 | 10 | export function evalRefsChangeMap(filePaths, isFileChanged, getRefs, maxDepth) { 11 | const refsChangeMap = {}; 12 | filePaths.forEach((filePath) => { 13 | if (refsChangeMap[filePath]) return; 14 | refsChangeMap[filePath] = evalRefsChange(filePath, 15 | isFileChanged, getRefs, refsChangeMap, maxDepth); 16 | logger.assert("set ref changes: %s %s", filePath, refsChangeMap[filePath]); 17 | }); 18 | return refsChangeMap; 19 | } 20 | 21 | function evalRefsChange(filePath, isFileChanged, getRefs, refsChangeMap, depth) { 22 | // Depth of deps analysis. 23 | if (depth === 0) { 24 | return RefsChangeType.NONE; 25 | } 26 | 27 | const refs = getRefs(filePath); 28 | if (!refs) { 29 | refsChangeMap[filePath] = RefsChangeType.NONE; 30 | return RefsChangeType.NONE; 31 | } 32 | 33 | const refsChange = isRefsChanged(filePath, isFileChanged, refs); 34 | if (refsChange !== RefsChangeType.NONE) { 35 | refsChangeMap[filePath] = refsChange; 36 | return refsChange; 37 | } 38 | 39 | const modules = refs.modules; 40 | for (const mPath of modules) { 41 | let result = refsChangeMap[mPath]; 42 | if (result === undefined) { 43 | result = evalRefsChange(mPath, isFileChanged, getRefs, refsChangeMap, depth - 1); 44 | } 45 | if (result !== RefsChangeType.NONE) { 46 | refsChangeMap[filePath] = RefsChangeType.MODULES; 47 | return RefsChangeType.MODULES; 48 | } 49 | } 50 | refsChangeMap[filePath] = RefsChangeType.NONE; 51 | return RefsChangeType.NONE; 52 | } 53 | 54 | function isRefsChanged(filePath, isFileChanged, refs) { 55 | function isFilesChanged(files) { 56 | if (!files) return false; 57 | 58 | const tLen = files.length; 59 | for (let i = 0; i < tLen; i++) { 60 | if (isFileChanged(files[i])) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | if (refs) { 68 | const typings = refs.refTypings; 69 | if (isFilesChanged(typings)) { 70 | logger.debug("referenced typings changed in %s", filePath); 71 | return RefsChangeType.TYPINGS; 72 | } 73 | 74 | const files = refs.refFiles; 75 | if (isFilesChanged(files)) { 76 | logger.debug("referenced files changed in %s", filePath); 77 | return RefsChangeType.FILES; 78 | } 79 | 80 | const modules = refs.modules; 81 | if (isFilesChanged(modules)) { 82 | logger.debug("imported module changed in %s", filePath); 83 | return RefsChangeType.MODULES; 84 | } 85 | } 86 | 87 | return RefsChangeType.NONE; 88 | } 89 | -------------------------------------------------------------------------------- /app/imports/courses/server/WordsApi.ts: -------------------------------------------------------------------------------- 1 | import { ACL } from "../../../lib/server/ACL"; 2 | import { SentencesApi } from "./SentencesApi"; 3 | 4 | var fs = Npm.require('fs'); 5 | 6 | export class WordsApi { 7 | static wordPictures: string[] = fs.readdirSync('../web.browser/app/').filter(fn => /\.svg$/.test(fn)).map(fn => fn.replace(/.svg$/, '')); 8 | 9 | @Decorators.publish 10 | static subscribeToWords(lessonId): Mongo.Cursor { 11 | var user = ACL.getUserOrThrow(this); 12 | return Words.find({ lessonId: lessonId }); 13 | } 14 | 15 | @Decorators.method 16 | static getWordPictures(callback?) { 17 | return WordsApi.wordPictures.reduce((a, wp) => { a[wp] = '/' + wp + '.svg'; return a }, {}); 18 | } 19 | 20 | @Decorators.method 21 | static getWordsForReuse(courseId, lessonId, callback?) { 22 | var user = ACL.getUserOrThrow(this); 23 | var course = Courses.findOne(courseId); 24 | if (!course) 25 | return []; 26 | var previousLessonIds = []; 27 | for (let i=0; i < course.tree.length; i++) { 28 | if (course.tree[i].lessons.some(l => l.id == lessonId)) 29 | break; 30 | course.tree[i].lessons.forEach(l => previousLessonIds.push(l.id)); 31 | } 32 | return Words.find({ lessonId: { $in: previousLessonIds } }, { fields: { text: 1 } }).fetch().map(w => w.text); 33 | } 34 | 35 | @Decorators.method 36 | static addWord(text: string, lessonId: string, callback?) { 37 | var user = ACL.getUserOrThrow(this); 38 | Words.insert({ 39 | text: text, 40 | lessonId: lessonId, 41 | translations: [], 42 | inflections: [] 43 | }); 44 | SentencesApi.refreshWordHints(text); 45 | } 46 | 47 | @Decorators.method 48 | static updateWord(wordModel: Word, callback?) { 49 | var user = ACL.getUserOrThrow(this); 50 | Words.update( 51 | { _id: wordModel._id }, 52 | { $set: { 53 | text: wordModel.text, 54 | remarks: wordModel.remarks, 55 | translations: wordModel.translations, 56 | inflections: wordModel.inflections 57 | } } 58 | ); 59 | SentencesApi.refreshWordHints(wordModel.text); 60 | for (var wordForm of wordModel.inflections) 61 | SentencesApi.refreshWordHints(wordForm.text); 62 | } 63 | 64 | @Decorators.method 65 | static removeWord(word, callback?) { 66 | var user = ACL.getUserOrThrow(this); 67 | Words.remove( 68 | { _id: word._id } 69 | ); 70 | SentencesApi.refreshWordHints(word.text); 71 | for (var wordForm of word.inflections) 72 | SentencesApi.refreshWordHints(wordForm.text); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /app/lib/client/Decorators.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue/dist/vue.min.js"; 2 | 3 | export function vueComponent(name: string, options?: any): (target: any) => any { 4 | return function(target: any) { 5 | // save a reference to the original constructor 6 | var original = target; 7 | // a utility function to generate instances of a class 8 | function construct(constructor, args) { 9 | var c: any = function () { 10 | return constructor.apply(this, args); 11 | } 12 | c.prototype = constructor.prototype; 13 | return new c(); 14 | } 15 | var vueInstanceFunctions = [ 16 | "init", 17 | "beforeCreate", 18 | "created", 19 | "beforeMount", 20 | "mounted", 21 | "beforeUpdate", 22 | "updated", 23 | "beforeDestroy", 24 | "destroyed" 25 | ]; 26 | if (!options) options = {}; 27 | options.template = VueTemplate[name]; 28 | if (!options.props) options.props = {}; 29 | if (!options.watch) options.watch = {}; 30 | if (!options.computed) options.computed = {}; 31 | if (options.data) { 32 | if (typeof options.data == 'function'){ 33 | var data_rtn = (options).data(); 34 | options.data = data_rtn; 35 | } 36 | } else options.data = {}; 37 | if (!options.methods) options.methods = {}; 38 | if (options['style']) delete options['style']; 39 | 40 | var newi = construct(original, {}); 41 | 42 | for(var key in newi){ 43 | if (key.charAt(0) != '$' && key.charAt(0) != '_'){ 44 | var prop_desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(newi), key); 45 | if (prop_desc && prop_desc.get) { 46 | var computed_obj:any = {}; 47 | if(prop_desc.set){ 48 | computed_obj.get = prop_desc.get; 49 | computed_obj.set = prop_desc.set; 50 | } else { 51 | computed_obj = prop_desc.get; 52 | } 53 | options.computed[key] = computed_obj; 54 | } 55 | else if (typeof(newi[key]) == 'function'){ 56 | if (vueInstanceFunctions.indexOf(key) > -1){ 57 | options[key] = newi[key] 58 | } else { 59 | if (key != 'constructor') 60 | options.methods[key] = newi[key]; 61 | } 62 | } else { 63 | options.data[key] = newi[key]; 64 | } 65 | } 66 | } 67 | 68 | var data = options.data; 69 | options.data = function() { return data; }; 70 | return function() { return Vue.component(name, options); }; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /app/public/dog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 15 | 17 | 18 | 22 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/imports/courses/client/CourseTreeComponent.ts: -------------------------------------------------------------------------------- 1 | import VueRouter, { Route } from "vue-router"; 2 | import { vueComponent } from "../../../lib/client/Decorators"; 3 | import { RepetitionIntervals } from "../../../lib/Db"; 4 | import { CoursesApi } from "../server/CoursesApi"; 5 | import "./course-tree.css"; 6 | 7 | @vueComponent("course-tree", { 8 | props: ['course', 'mode'], 9 | }) 10 | export class CourseTreeComponent 11 | { 12 | $route: Route; 13 | $router: VueRouter; 14 | course: Course; 15 | mode: 'edit' | 'study'; 16 | 17 | showIconEditorForLesson: Lesson = null; 18 | lessonStatus = {}; 19 | 20 | created() { 21 | var notAvailable = false; 22 | var now = Date.now(); 23 | var msInHour = 60 * 60 * 1000; 24 | var isCompleted = l => l.disabled || Meteor.user().study && Meteor.user().study.completedLessonIds.indexOf(l.id) > -1; 25 | var wordDecayFactor = w => Math.max(0, (now - w.lastDate) / msInHour - RepetitionIntervals["Level" + w.bucket]) / RepetitionIntervals["Level" + w.bucket]; 26 | var decayIndex = l => { 27 | if (!isCompleted(l)) 28 | return 5; 29 | let words = Meteor.user().study.learnedWords.filter(w => w.lessonId == l.id); 30 | let sum = words.reduce((a, w) => a + Math.min(5, wordDecayFactor(w)), 0); 31 | return Math.floor(sum / words.length); 32 | }; 33 | 34 | for (var row of this.course.tree) { 35 | if (notAvailable) 36 | row.lessons.forEach(l => this.lessonStatus[l.id] = 'locked'); 37 | else { 38 | row.lessons.forEach(l => this.lessonStatus[l.id] = 'decay' + decayIndex(l)); 39 | if (!row.lessons.every(l => isCompleted(l))) 40 | notAvailable = true; 41 | } 42 | } 43 | 44 | } 45 | 46 | mounted() { 47 | this.showIconEditorForLesson = null; 48 | } 49 | 50 | getLessonColor(lesson) { 51 | if (this.mode === 'edit') { 52 | if (lesson.disabled) 53 | return 'locked'; 54 | return ''; 55 | } else 56 | return this.lessonStatus[lesson.id]; 57 | } 58 | 59 | removeLesson(row, lesson) { 60 | if (!confirm('Are you sure want to remove lesson "' + lesson.name + "'? This action cannot be undone!")) 61 | return; 62 | row.lessons.splice(row.lessons.indexOf(lesson),1); 63 | if (row.lessons.length == 0 && this.course.tree.indexOf(row) == this.course.tree.length - 1) 64 | this.course.tree.splice(this.course.tree.length - 1, 1); 65 | this.saveCourse(); 66 | } 67 | 68 | clickLesson(lesson) { 69 | if (this.mode == 'edit') 70 | this.showIconEditorForLesson = lesson; 71 | else if (this.lessonStatus[lesson.id] != 'locked') 72 | this.$router.push('/study/' + this.course._id + '/lessons/' + lesson.id); 73 | } 74 | 75 | saveCourse() { 76 | CoursesApi.updateCourse(this.course); 77 | } 78 | } -------------------------------------------------------------------------------- /app/lib/Db.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | 3 | interface FinnlingoUser { 4 | 5 | _id?: string; 6 | username?: string; 7 | emails?: Meteor.UserEmail[]; 8 | createdAt?: Date; 9 | profile?: { 10 | name?: string; 11 | photo?: string; 12 | }; 13 | services?: any; 14 | 15 | selectedCourseId: string; 16 | study: { 17 | dailyGoal: number; 18 | daysStudied: number; 19 | lastDateStudied: number; 20 | lastDateXP: number; 21 | streakDays: number; 22 | streakLastDate: number; 23 | xp: number; 24 | completedLessonIds: string[]; 25 | learnedWords: { id: string; lessonId: string; lastDate: number; bucket: number; }[]; 26 | } 27 | 28 | } 29 | 30 | interface TextWithRemarks { 31 | text: string; 32 | remarks: string; 33 | } 34 | 35 | interface Word 36 | { 37 | _id?: string; 38 | text: string; 39 | lessonId: string; 40 | remarks?: string; 41 | audio?: string; 42 | picture?: string; 43 | inflections: TextWithRemarks[]; 44 | translations: TextWithRemarks[]; 45 | } 46 | var Words: Mongo.Collection; 47 | 48 | interface Sentence 49 | { 50 | _id?: string; 51 | text: string; 52 | lessonId: string; 53 | order: number; 54 | testType: SentenceTestType; 55 | translations: TextWithRemarks[]; 56 | backTranslations: TextWithRemarks[]; 57 | wordHints: { [word: string]: { wordId: string; translations: string[] } }; 58 | editor: { _id: string, avatarUrl: string, name: string }; 59 | author: { _id: string, avatarUrl: string, name: string }; 60 | } 61 | var Sentences: Mongo.Collection; 62 | 63 | interface Lesson { 64 | id: string; 65 | name: string; 66 | icon: string; 67 | isOptional?: boolean; 68 | disabled?: boolean; 69 | } 70 | 71 | interface Course 72 | { 73 | _id?: string; 74 | name: string; 75 | tree: { lessons: Lesson[]; }[]; 76 | admin_ids: string[]; 77 | } 78 | var Courses: Mongo.Collection; 79 | 80 | } 81 | 82 | // in hours 83 | export enum RepetitionIntervals { 84 | Level0 = 0, 85 | Level1 = 5, 86 | Level2 = 24, 87 | Level3 = 5 * 24, 88 | Level4 = 25 * 24, 89 | Level5 = 120 * 24, 90 | Level6 = 720 * 24 91 | } 92 | (this as any).RepetitionIntervals = RepetitionIntervals; 93 | 94 | export enum SentenceTestType { 95 | Default, 96 | WordPictures, 97 | SelectMissingWord, 98 | ConstructSentence, 99 | Notes 100 | } 101 | (this as any).SentenceTestType = SentenceTestType; 102 | 103 | Sentences = new Mongo.Collection("sentences"); 104 | Words = new Mongo.Collection("words"); 105 | Courses = new Mongo.Collection("courses"); 106 | -------------------------------------------------------------------------------- /app/public/harbour.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 17 | 18 | 20 | 21 | 23 | 25 | 26 | 28 | 29 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/public/day.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 11 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 25 | 27 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/options.js: -------------------------------------------------------------------------------- 1 | var ts = require("typescript"); 2 | var _ = require("underscore"); 3 | 4 | function presetCompilerOptions(customOptions) { 5 | if (! customOptions) return; 6 | 7 | var compilerOptions = customOptions; 8 | 9 | // Declaration files are expected to 10 | // be generated separately. 11 | compilerOptions.declaration = false; 12 | 13 | // Overrides watching, 14 | // it is handled by Meteor itself. 15 | compilerOptions.watch = false; 16 | 17 | // We use source maps via Meteor file API, 18 | // This class's API provides source maps 19 | // separately but alongside compilation results. 20 | // Hence, skip generating inline source maps. 21 | compilerOptions.inlineSourceMap = false; 22 | compilerOptions.inlineSources = false; 23 | 24 | // Always emit. 25 | compilerOptions.noEmit = false; 26 | compilerOptions.noEmitOnError = false; 27 | 28 | // Don't generate any files, hence, 29 | // skip setting outDir and outFile. 30 | compilerOptions.outDir = null; 31 | compilerOptions.outFile = null; 32 | 33 | // This is not need as well. 34 | // API doesn't have paramless methods. 35 | compilerOptions.rootDir = null; 36 | compilerOptions.sourceRoot = null; 37 | 38 | return compilerOptions; 39 | } 40 | 41 | exports.presetCompilerOptions = presetCompilerOptions; 42 | 43 | // Default compiler options. 44 | function getDefaultCompilerOptions(arch) { 45 | var options = { 46 | target: "es5", 47 | module : "commonjs", 48 | moduleResolution: "node", 49 | sourceMap: true, 50 | noResolve: false, 51 | lib: ["es5"], 52 | diagnostics: true, 53 | noEmitHelpers: true, 54 | // Always emit class metadata, 55 | // especially useful for Angular2. 56 | emitDecoratorMetadata: true, 57 | // Support decorators by default. 58 | experimentalDecorators: true, 59 | // Don't impose `use strict` 60 | noImplicitUseStrict: true, 61 | baseUrl: ".", 62 | rootDirs: ["."], 63 | }; 64 | 65 | if (/^web/.test(arch)) { 66 | options.lib.push("dom"); 67 | } 68 | 69 | return options; 70 | } 71 | 72 | exports.getDefaultCompilerOptions = getDefaultCompilerOptions; 73 | 74 | // Validate compiler options and convert them from 75 | // user-friendly format to enum values used by TypeScript, e.g.: 76 | // 'system' string converted to ts.ModuleKind.System value. 77 | function convertCompilerOptionsOrThrow(options) { 78 | if (! options) return null; 79 | 80 | var result = ts.convertCompilerOptionsFromJson(options, ""); 81 | 82 | if (result.errors && result.errors.length) { 83 | throw new Error(result.errors[0].messageText); 84 | } 85 | 86 | return result.options; 87 | } 88 | 89 | exports.convertCompilerOptionsOrThrow = convertCompilerOptionsOrThrow; 90 | 91 | function validateTsConfig(configJson) { 92 | var result = ts.parseJsonConfigFileContent(configJson, ts.sys, ""); 93 | 94 | if (result.errors && result.errors.length) { 95 | throw new Error(result.errors[0].messageText); 96 | } 97 | } 98 | 99 | exports.validateTsConfig = validateTsConfig; 100 | -------------------------------------------------------------------------------- /app/public/night.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/public/apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 16 | 19 | 22 | 23 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/imports/courses/client/course-tree.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/imports/study/client/study.css: -------------------------------------------------------------------------------- 1 | .content.study { 2 | margin-bottom: 60px; 3 | } 4 | 5 | .study .sentence .word-with-hint { 6 | text-decoration-style: dashed; 7 | color: #444; 8 | } 9 | .study .sentence .word-with-hint.new { 10 | color: #D88700; 11 | font-weight: bold; 12 | } 13 | 14 | .study .sentence .hint { 15 | position: absolute; 16 | background: rgba(0,0,0,0.8); 17 | padding: 5px 15px; 18 | margin-top: 28px; 19 | border-radius: 5px; 20 | font-size: 18px; 21 | color: #D8D7D6; 22 | } 23 | .study .sentence .hint .remark, 24 | .study .sentence .hint .translation { 25 | display: block; 26 | } 27 | 28 | .study .check-button { 29 | background-color: #87D788; 30 | font-size: 20px; 31 | text-align: center; 32 | width: 90%; 33 | border-radius: 15px; 34 | color: #fff; 35 | text-transform: uppercase; 36 | text-decoration: none; 37 | display: block; 38 | } 39 | 40 | .study .check-button.disabled { 41 | background-color: #D8E7D6; 42 | cursor: default; 43 | } 44 | 45 | .study .answer { 46 | padding-top: 10px; 47 | padding-bottom: 10px; 48 | } 49 | .study .answer input { 50 | width: 90%; 51 | } 52 | 53 | .study .result-popup { 54 | position: absolute; 55 | margin-bottom: -80px; 56 | background: #D8D7D6; 57 | padding: 10px 15px; 58 | border-radius: 12px; 59 | } 60 | 61 | .study .word-picture { 62 | width: 30%; 63 | display: inline-block; 64 | padding-bottom: 10px; 65 | padding-left: 10%; 66 | } 67 | 68 | .study .construction { 69 | margin-bottom: 15px; 70 | } 71 | .study .construction .answer { 72 | border: 1px solid #E8E7E6; 73 | min-height: 50px; 74 | margin-bottom: 10px; 75 | } 76 | .study .construction .word { 77 | background: #E8E7E6; 78 | display: inline-block; 79 | margin: 4px 9px; 80 | padding: 1px 15px; 81 | border-radius: 3px; 82 | box-shadow: inset 0 0 2px #B8B7B6; 83 | height: 25px; 84 | vertical-align: top; 85 | cursor: pointer; 86 | } 87 | .study .construction .word.selected { 88 | background: #fff; 89 | cursor: default; 90 | } 91 | 92 | 93 | .study .attribution-bar { 94 | position: fixed; 95 | bottom: 0; 96 | left: 50%; 97 | transform: translateX(-50%); 98 | font-size: 14px; 99 | color: #444; 100 | padding: 7px 20px; 101 | border-top-left-radius: 10px; 102 | border-top-right-radius: 10px; 103 | background: #E8E7E6; 104 | width: calc(90% - 40px); 105 | max-width: 540px; 106 | } 107 | 108 | .study .attribution-bar a { 109 | color: #333; 110 | } 111 | 112 | .study .finished { 113 | text-align: center; 114 | } 115 | .study .plus-xp { 116 | color: #444; 117 | font-size: 68px; 118 | } 119 | .study .plus-xp .fa { 120 | display: block; 121 | font-size: 150px; 122 | } 123 | 124 | .study .finished .check-button { 125 | margin: 0 auto; 126 | } 127 | 128 | .study .progress-bar{ 129 | width: 90%; 130 | height: 4px; 131 | background-color: #D7D9D8; 132 | border-radius: 4px; 133 | } 134 | 135 | .study .progress-bar-progress{ 136 | height: 4px; 137 | background-color: #87D788; 138 | border-radius: 4px; 139 | display: inherit; 140 | } 141 | .finished-enter-active { 142 | transition: all ease-out 1s; 143 | } 144 | .finished-enter { 145 | opacity: 0; 146 | margin-bottom: -30px; 147 | padding-top: 30px; 148 | } -------------------------------------------------------------------------------- /app/public/bus-stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 30 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/public/broken-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/imports/courses/client/course-tree.css: -------------------------------------------------------------------------------- 1 | .course-tree input { 2 | border: 0; 3 | } 4 | .course-tree input:hover { 5 | background-color: #FED427; 6 | } 7 | 8 | 9 | .course-tree h2 input { 10 | font-size: 48px; 11 | margin-left: 20px; 12 | } 13 | 14 | .course-tree .row { 15 | text-align: center; 16 | } 17 | 18 | .course-tree.study-mode .row { 19 | border: 0; 20 | } 21 | 22 | .course-tree .lesson { 23 | position: relative; 24 | display: inline-block; 25 | width: 100px; 26 | padding: 12px; 27 | } 28 | .course-tree .lesson a, 29 | .course-tree .lesson input { 30 | display: block; 31 | max-width: 90px; 32 | text-align: center; 33 | font-size: 16px; 34 | font-weight: bold; 35 | color: #222; 36 | text-decoration: none; 37 | } 38 | .course-tree .lesson-icon { 39 | font-size: 48px; 40 | padding-bottom: 8px; 41 | padding-top: 10px; 42 | border-radius: 60px; 43 | width: 70px; 44 | height: 50px; 45 | margin-bottom: 3px; 46 | color: #FFF; 47 | background: #A0DB20; 48 | } 49 | .course-tree .locked .lesson-icon { 50 | color: #D8D7D6; 51 | background: #EFEEED; 52 | box-shadow: 0px 0px 2px #888; 53 | } 54 | .course-tree .decay0 .lesson-icon { 55 | color: #EF8000; 56 | background: repeating-linear-gradient(120deg,#FBB030 0,#FBB030 20px,#FAA020 20px,#FAA020 28px); 57 | box-shadow: 0px 0px 2px #871; 58 | } 59 | .course-tree .decay0 .lesson-icon:hover { 60 | color: #F48F20; 61 | } 62 | .course-tree .decay1 .lesson-icon { 63 | color: #FFF; 64 | background: #dcb15b; 65 | } 66 | .course-tree .decay2 .lesson-icon { 67 | color: #FFF; 68 | background: #90C070; 69 | } 70 | .course-tree .decay3 .lesson-icon { 71 | color: #EEE; 72 | background: linear-gradient(120deg,#90C070,#90C070 30%, #90A094, #90A094 100%); 73 | } 74 | .course-tree .decay4 .lesson-icon { 75 | color: #EEE; 76 | background: #90A094; 77 | } 78 | .course-tree .decay5 .lesson-icon { 79 | color: #EEE; 80 | background: #D8D7D6; 81 | } 82 | 83 | .course-tree .add-lesson a i { 84 | font-size: 112px; 85 | color: #E7E9E8; 86 | margin-left: 20px; 87 | } 88 | 89 | .course-tree .lesson .remove-link { 90 | position: absolute; 91 | font-size: 14px; 92 | top: 10px; 93 | right: 22px; 94 | color: #848484; 95 | width: 26px; 96 | height: 22px; 97 | border-radius: 100%; 98 | padding-top: 4px; 99 | background-color: rgba(255, 255, 255, 0.5); 100 | border: 1px solid #E0DFDE; 101 | } 102 | .course-tree .lesson .icon-link { 103 | position: absolute; 104 | font-size: 14px; 105 | bottom: 34px; 106 | right: 22px; 107 | color: #848484; 108 | width: 24px; 109 | height: 23px; 110 | border-radius: 100%; 111 | padding-left: 2px; 112 | padding-top: 3px; 113 | background-color: rgba(255, 255, 255, 0.5); 114 | border: 1px solid #E0DFDE; 115 | } 116 | .course-tree .lesson .disable-link { 117 | position: absolute; 118 | font-size: 14px; 119 | bottom: 34px; 120 | left: 22px; 121 | color: #848484; 122 | width: 24px; 123 | height: 23px; 124 | border-radius: 100%; 125 | padding-left: 2px; 126 | padding-top: 3px; 127 | background-color: rgba(255, 255, 255, 0.5); 128 | border: 1px solid #E0DFDE; 129 | } 130 | 131 | .icon-editor { 132 | text-align: center; 133 | } 134 | .icon-editor a { 135 | font-size: 48px; 136 | padding: 8px; 137 | color: #848484; 138 | display: inline-block; 139 | width: 70px; 140 | height: 50px; 141 | } 142 | 143 | .icon-editor a > i.fa { 144 | color: #848484; 145 | } 146 | -------------------------------------------------------------------------------- /app/public/boy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 30 | 32 | 34 | 35 | 36 | 37 | 39 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/imports/courses/server/SentencesApi.ts: -------------------------------------------------------------------------------- 1 | import { SentenceTestType } from "../../../lib/Db"; 2 | import { ACL } from "../../../lib/server/ACL"; 3 | import { Utilities } from "../../../lib/Utilities"; 4 | 5 | export class SentencesApi { 6 | @Decorators.publish 7 | static subscribeToSentences(lessonId): Mongo.Cursor { 8 | var user = ACL.getUserOrThrow(this); 9 | return Sentences.find({ lessonId: lessonId }); 10 | } 11 | 12 | @Decorators.method 13 | static addSentence(text: string, lessonId: string, callback?) { 14 | var user = ACL.getUserOrThrow(this); 15 | var userDisplayInfo = { _id: user._id, avatarUrl: "http://graph.facebook.com/" + user.services.facebook.id + "/picture", name: user.profile.name }; 16 | var tmpSentences = Sentences.find({ lessonId: lessonId }, { sort: { order: -1 }}).fetch(); 17 | Sentences.insert({ 18 | text: text, 19 | testType: SentenceTestType.Default, 20 | translations: [], 21 | backTranslations: [], 22 | lessonId: lessonId, 23 | order: tmpSentences.length > 0 ? tmpSentences[0].order + 1 : 0,// (Sentences.find({ lessonId: lessonId }, { sort: { order: 0 }})[0].order++), 24 | wordHints: SentencesApi.generateWordHints(lessonId, text), 25 | author: userDisplayInfo, 26 | editor: userDisplayInfo, 27 | }); 28 | } 29 | 30 | @Decorators.method 31 | static updateSentence(sentenceModel: Sentence, callback?) { 32 | var user = ACL.getUserOrThrow(this); 33 | var userDisplayInfo = { _id: user._id, avatarUrl: "http://graph.facebook.com/" + user.services.facebook.id + "/picture", name: user.profile.name }; 34 | 35 | Sentences.update( 36 | { _id: sentenceModel._id }, 37 | { $set: { 38 | text: sentenceModel.text, 39 | testType: sentenceModel.testType, 40 | translations: sentenceModel.translations, 41 | backTranslations: sentenceModel.backTranslations, 42 | order: sentenceModel.order, 43 | wordHints: SentencesApi.generateWordHints(sentenceModel.lessonId, sentenceModel.text), 44 | editor: userDisplayInfo 45 | } } 46 | ); 47 | } 48 | 49 | static generateWordHints(lessonId: string, text: string) { 50 | let lessonIds = []; 51 | Courses.findOne({ "tree.lessons.id": lessonId }).tree.forEach(r => r.lessons.forEach(l => lessonIds.push(l.id))); 52 | let wordHints = {}; 53 | for (let word of Utilities.sentenceToWords(text)) { 54 | let wordObj = Words.findOne({ lessonId: { $in: lessonIds }, $or: [{ text: word }, { "inflections.text": word }]}); 55 | let html = ''; 56 | if (wordObj) { 57 | let inflection = wordObj.inflections.filter(i => i.text == word)[0]; 58 | wordHints[word] = { wordId: wordObj._id, translations: inflection ? [ inflection.remarks ] : wordObj.translations.map(t => t.text) }; 59 | } 60 | } 61 | return wordHints; 62 | } 63 | 64 | // this is a very heavy operation 65 | // probably need to rewrite later 66 | static refreshWordHints(word) { 67 | var sentences = Sentences.find({ text: { $regex: word.replace(/[^a-zäö]/g,''), $options: 'i' } }, { fields: { text: 1, lessonId: 1 } }).fetch(); 68 | for (var sentence of sentences) { 69 | Sentences.update( 70 | { _id: sentence._id }, 71 | { $set: { 72 | wordHints: SentencesApi.generateWordHints(sentence.lessonId, sentence.text) 73 | } } 74 | ); 75 | } 76 | } 77 | 78 | @Decorators.method 79 | static removeSentence(sentence, callback?) { 80 | var user = ACL.getUserOrThrow(this); 81 | Sentences.remove( 82 | { _id: sentence._id } 83 | ); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /app/public/woman.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 35 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/imports/study/client/StudyComponent.ts: -------------------------------------------------------------------------------- 1 | import { Route } from "vue-router"; 2 | import { vueComponent } from "../../../lib/client/Decorators"; 3 | import { SentenceTestType } from "../../../lib/Db"; 4 | import { Utilities } from "../../../lib/Utilities"; 5 | import { StudyApi } from "../server/StudyApi"; 6 | import "./study.css"; 7 | 8 | export enum CheckResult { 9 | None, 10 | Fail, 11 | Success 12 | } 13 | 14 | @vueComponent('study') 15 | export class StudyComponent { 16 | $route: Route; 17 | $set: Function; 18 | $nextTick: Function; 19 | 20 | sentences: Sentence[] = []; 21 | showHint: string = ""; 22 | index = 0; 23 | answer = ''; 24 | result: CheckResult = CheckResult.None; 25 | finished: any = null; 26 | showXP: boolean = false; 27 | wordFailures: { [id: number]: number } = {}; 28 | selectedWords: any[] = []; 29 | selectedOptions: { [index: number]: string} = {}; 30 | 31 | created() { 32 | this.getSentences(); 33 | } 34 | 35 | mounted() { 36 | this.index = 0; 37 | this.result = CheckResult.None; 38 | this.answer = ''; 39 | this.finished = null; 40 | this.showXP = false; 41 | this.selectedWords = []; 42 | this.selectedOptions = {}; 43 | } 44 | 45 | async getSentences() { 46 | const result = await StudyApi.getSentences(this.$route.params.lessonid); 47 | this.sentences = result.sentences; 48 | this.wordFailures = {}; 49 | for (let s of this.sentences) { 50 | for (let w in s.wordHints) 51 | this.wordFailures[s.wordHints[w].wordId] = 0; 52 | } 53 | } 54 | 55 | selectWord(word, index) { 56 | if (this.selectedOptions[index]) 57 | return; 58 | this.selectedWords.push(word); 59 | this.$set(this.selectedOptions, index, true); 60 | } 61 | 62 | unselectWord(word) { 63 | this.selectedWords.splice(this.selectedWords.indexOf(word),1); 64 | for (let i = 0; i < this.sentences[this.index]["options"].length; i++) { 65 | if (this.selectedOptions[i] && this.sentences[this.index]["options"][i] == word) 66 | this.$set(this.selectedOptions, i, false); 67 | } 68 | } 69 | 70 | async check() { 71 | if (this.result == CheckResult.None) { 72 | if (!this.answer && this.selectedWords.length == 0) 73 | return; 74 | if (!this.answer) 75 | this.answer = this.selectedWords.join(' '); 76 | var answer = Utilities.sentenceToWords(this.answer).join(' '); 77 | if (this.sentences[this.index].testType == SentenceTestType.SelectMissingWord && this.sentences[this.index].translations[0].text == this.answer) { 78 | this.result = CheckResult.Success; 79 | } else if (this.sentences[this.index].testType != SentenceTestType.SelectMissingWord && this.sentences[this.index].translations.some(t => answer == Utilities.sentenceToWords(t.text).join(' '))) { 80 | this.result = CheckResult.Success; 81 | } else { 82 | this.result = CheckResult.Fail; 83 | for (var w in this.sentences[this.index].wordHints) 84 | this.wordFailures[this.sentences[this.index].wordHints[w].wordId]++; 85 | this.sentences.push({ ...this.sentences[this.index] }); 86 | } 87 | } else { 88 | this.result = CheckResult.None; 89 | this.answer = ''; 90 | this.selectedWords = []; 91 | this.selectedOptions = {}; 92 | if (this.index < this.sentences.length - 1) { 93 | this.index++; 94 | if (this.sentences[this.index].testType == SentenceTestType.Notes) 95 | this.result = CheckResult.Success; 96 | } else { 97 | this.finished = await StudyApi.finishLesson(this.$route.params.lessonid, this.wordFailures); 98 | this.showXP = false; 99 | this.$nextTick(() => this.showXP = true); 100 | } 101 | } 102 | } 103 | 104 | 105 | } -------------------------------------------------------------------------------- /app/main/client/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | border: 0; 6 | font-size: 20px; 7 | font-family: 'Dosis', 'Raleway', sans-serif; 8 | } 9 | 10 | input, select, textarea { 11 | font-family: 'Dosis', 'Raleway', sans-serif; 12 | font-size: 20px; 13 | } 14 | 15 | .dimmer { 16 | position: fixed; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | right: 0; 21 | background-color: rgba(0,0,0,0.8); 22 | } 23 | .dimmer.transparent { 24 | background-color: rgba(0,0,0,0); 25 | } 26 | 27 | div.clearfix { 28 | padding: 0; 29 | margin: 0; 30 | height: 0; 31 | } 32 | .clearfix:after { 33 | content: ''; 34 | display: table; 35 | clear: both; 36 | padding: 0; 37 | margin: 0; 38 | height: 0; 39 | } 40 | 41 | .popup 42 | { 43 | position: fixed; 44 | top: 50%; 45 | left: 50%; 46 | max-width: 400px; 47 | min-width: 150px; 48 | min-height: 80px; 49 | transform: translate(-50%, -50%); 50 | font-size: 20px; 51 | background: rgba(250,250,250,0.8); 52 | padding: 10px 15px; 53 | border-radius: 12px; 54 | } 55 | .popup div 56 | { 57 | 58 | font-size: 28px; 59 | padding: 8px; 60 | } 61 | 62 | .row { 63 | padding-top: 12px; 64 | padding-left: 12px; 65 | padding-right: 12px; 66 | padding-bottom: 19px; 67 | border-top: 1px solid #D7D9D8; 68 | } 69 | .row.selected { 70 | box-shadow: inset 0 0 25px #E8E8E7; 71 | } 72 | .row.warning { 73 | border-left: 4px solid #D88700; 74 | } 75 | 76 | .list-editor a > i.fa { 77 | color: #D7D9D8; 78 | padding-left: 4px; 79 | } 80 | .list-editor a > i.fa:hover { 81 | color: #B7B9B8; 82 | } 83 | 84 | .content { 85 | margin: 0 auto; 86 | max-width: 600px; 87 | padding-left: 15px; 88 | padding-right: 15px; 89 | } 90 | 91 | .content.wide { 92 | max-width: 1200px; 93 | } 94 | 95 | @media (max-width: 800px) { 96 | .content { 97 | padding: 0; 98 | } 99 | } 100 | 101 | 102 | .top-bar { 103 | background-color: #E8E8E7; 104 | padding: 12px; 105 | border-bottom: 1px solid #D7D9D8; 106 | margin-bottom: 7px; 107 | height: 21px; 108 | margin-left: -15px; 109 | margin-right: -15px; 110 | } 111 | @media (max-width: 800px) { 112 | .top-bar { 113 | margin-left: 0; 114 | margin-right: 0; 115 | } 116 | .course-tree > h2, 117 | .courses > h2, 118 | .courses > .description { 119 | margin-left: 15px; 120 | margin-right: 15px; 121 | } 122 | } 123 | .top-bar .xp { 124 | float: right; 125 | font-weight: bold; 126 | } 127 | 128 | .small-gray-link { 129 | color: #D7D9D8; 130 | text-decoration: none; 131 | font-size: 15px; 132 | display: inline-block; 133 | margin-left: 10px; 134 | } 135 | 136 | .avatar { 137 | border-radius: 100%; 138 | } 139 | .row .avatar { 140 | height: 30px; 141 | vertical-align: top; 142 | } 143 | 144 | .dashboard.content { 145 | position: relative; 146 | } 147 | .dashboard .course-tree { 148 | margin-right: 240px; 149 | } 150 | @media (max-width: 1000px) { 151 | .dashboard .course-tree { 152 | margin-right: 0; 153 | } 154 | } 155 | 156 | .sidemenu { 157 | text-align: right; 158 | padding-right: 20px; 159 | } 160 | .sidemenu a { 161 | color: #B7B9B8; 162 | } 163 | .sidebar { 164 | width: 220px; 165 | position: absolute; 166 | right: 0; 167 | } 168 | .sidebar .name { 169 | width: 120px; 170 | overflow: hidden; 171 | white-space: nowrap; 172 | text-overflow: ellipsis; 173 | display: inline-block; 174 | vertical-align: top; 175 | } 176 | .sidebar .xp { 177 | float: right; 178 | } 179 | @media (max-width: 1000px) { 180 | .sidebar { 181 | position: static; 182 | right: auto; 183 | max-width: auto; 184 | } 185 | } 186 | .sidebar h4 { 187 | padding-left: 10px; 188 | padding-right: 10px; 189 | margin-bottom: 0; 190 | border-top-left-radius: 4px; 191 | border-top-right-radius: 4px; 192 | background-color: #D7D9D8; 193 | } 194 | .sidebar ol { 195 | padding-bottom: 5px; 196 | padding-top: 2px; 197 | border-bottom-left-radius: 4px; 198 | border-bottom-right-radius: 4px; 199 | margin-bottom: 20px; 200 | margin-top: 5px; 201 | border-bottom: 3px solid #D7D9D8; 202 | font-size: 17px; 203 | padding-left: 20px; 204 | } 205 | .sidebar ol .avatar { 206 | height: 24px; 207 | vertical-align: top; 208 | } 209 | .sidebar ol li { 210 | margin-top: 7px; 211 | margin-bottom: 7px; 212 | } 213 | 214 | .description { 215 | font-size: 14px; 216 | color: #444; 217 | } 218 | -------------------------------------------------------------------------------- /app/public/mouse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 14 | 17 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 41 | 42 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/public/bird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 17 | 18 | 20 | 26 | 27 | 28 | 31 | 34 | 37 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/packages/vue-templates/vue-templates.js: -------------------------------------------------------------------------------- 1 | var VueTemplate; 2 | 3 | (function() { 4 | 5 | Plugin.registerCompiler({ 6 | extensions: ['html'], 7 | archMatching: 'web', 8 | isTemplate: true 9 | }, function () { return new VueTemplatesCompiler() }); 10 | 11 | var VueTemplatesCompiler = function () { }; 12 | VueTemplatesCompiler.prototype.processFilesForTarget = function (files) { 13 | var templatesJs = ""; 14 | files.forEach(function (file) { 15 | var contents = file.getContentsAsString(); 16 | var nodes = parseHtml(contents); 17 | 18 | for (var i=0;i f.getBasename() == "VueTemplates_all.html")[0]; 33 | templatesFile.addJavaScript({ data: templatesJs, path: templatesFile.getBasename() }); 34 | } 35 | }; 36 | 37 | function parseHtml(html) { 38 | var startTagRegex = /^<(!?[-A-Za-z0-9_]+)((?:\s+[\w\-\:\.]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, 39 | endTagRegex = /^<\/([-A-Za-z0-9_]+)[^>]*>/; 40 | var special = { script: 1, style: 1 }; 41 | var index, chars, match, stack = [], last = html; 42 | stack.last = function () { 43 | return this[this.length - 1]; 44 | }; 45 | var Node = function(tagName, attrs, parent) { 46 | this.tagName = tagName; 47 | this.attrs = attrs; 48 | this.childNodes = []; 49 | this.parentNode = parent; 50 | this.innerHTML = ""; 51 | }; 52 | Node.prototype.appendChild = function(tagName, attrs) { 53 | var newNode = new Node(tagName, attrs, this); 54 | this.childNodes.push(newNode); 55 | return newNode; 56 | }; 57 | Node.prototype.appendInnerHTML = function (html) { 58 | var node = this; 59 | while (node) { 60 | node.innerHTML += html; 61 | node = node.parentNode; 62 | } 63 | }; 64 | var currentNode = new Node(); 65 | var rootNode = currentNode; 66 | 67 | while (html) { 68 | chars = true; 69 | 70 | // Make sure we're not in a script or style element 71 | if (!stack.last() || !special[stack.last()]) { 72 | 73 | // Comment 74 | if (html.indexOf(""); 76 | 77 | if (index >= 0) { 78 | html = html.substring(index + 3); 79 | chars = false; 80 | } 81 | 82 | // end tag 83 | } else if (html.indexOf("")); 122 | } 123 | 124 | if (html == last) { 125 | console.log("Parse Error: " + html); 126 | return rootNode; 127 | } 128 | last = html; 129 | } 130 | 131 | return rootNode.childNodes; 132 | 133 | } 134 | })(); -------------------------------------------------------------------------------- /app/public/man.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 30 | 32 | 34 | 35 | 36 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/imports/courses/client/lesson-editor.css: -------------------------------------------------------------------------------- 1 | .lesson-editor { 2 | height: 100vh; 3 | background-color: #D9D8D7; 4 | position: relative; 5 | } 6 | .lesson-editor > .top-bar { 7 | position: absolute; 8 | left: 0; 9 | right: 0; 10 | height: 30px; 11 | } 12 | .lesson-editor .top-bar .back-link { 13 | position: absolute; 14 | } 15 | .lesson-editor .top-bar { 16 | margin-left: 0; 17 | margin-right: 0; 18 | overflow: hidden; 19 | } 20 | .lesson-editor .top-bar .title { 21 | margin-left: 40px; 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | max-width: calc(100vw - 184px); 25 | display: inline-block; 26 | overflow: hidden; 27 | word-break: break-all; 28 | vertical-align: top; 29 | } 30 | .lesson-editor .words-column, 31 | .lesson-editor .sentences-column, 32 | .lesson-editor .editor-column { 33 | background-color: #fff; 34 | float: left; 35 | height: calc(100% - 100px); 36 | overflow: auto; 37 | border-radius: 10px; 38 | margin-left: 25px; 39 | margin-bottom: 15px; 40 | margin-top: 75px; 41 | } 42 | 43 | .lesson-editor .words-column { 44 | width: 20%; 45 | } 46 | .lesson-editor .sentences-column { 47 | width: 36%; 48 | } 49 | .lesson-editor .editor-column { 50 | position: relative; 51 | width: 36%; 52 | transition: width 0.2s ease-out, margin-left 0.2s ease-in, margin-right 0.2s ease-in; 53 | } 54 | .lesson-editor .editor-column.hidden { 55 | width: 0; 56 | margin-left: 0; 57 | margin-right: 0; 58 | } 59 | .lesson-editor h4 { 60 | margin-left: 10px; 61 | } 62 | .lesson-editor p { 63 | margin-left: 10px; 64 | } 65 | .lesson-editor .editor-column img, 66 | .lesson-editor .editor-column input, 67 | .lesson-editor .editor-column select { 68 | margin-left: 10px; 69 | } 70 | .lesson-editor .editor-column textarea { 71 | margin-left: 10px; 72 | width: 90%; 73 | } 74 | .lesson-editor .editor-column .back-translations strong { 75 | margin-left: 10px; 76 | } 77 | 78 | .lesson-editor .editor-column .word-picture { 79 | height: 80px; 80 | width: auto; 81 | display: inline-block; 82 | vertical-align: top; 83 | } 84 | 85 | .lesson-editor .words-column input { 86 | max-width: 60%; 87 | } 88 | 89 | .lesson-editor input.text { 90 | width: 20%; 91 | } 92 | .lesson-editor input.remarks { 93 | width: 50%; 94 | } 95 | 96 | .lesson-editor .word-with-hint { 97 | color: #00A090; 98 | } 99 | 100 | @media (max-width: 1000px) { 101 | .lesson-editor .top-bar .menu { 102 | margin: 0 auto; 103 | display: block; 104 | width: 200px; 105 | padding-left: 40px; 106 | } 107 | .lesson-editor .top-bar .menu a { 108 | padding: 3px 9px; 109 | padding-left: 12px; 110 | border-radius: 10px; 111 | background-color: #E8E7E6; 112 | color: #777; 113 | text-decoration: none; 114 | } 115 | .lesson-editor .top-bar .menu .selected { 116 | font-weight: bold; 117 | text-decoration: underline; 118 | } 119 | 120 | .lesson-editor .words-column, 121 | .lesson-editor .sentences-column, 122 | .lesson-editor .editor-column { 123 | margin-top: 95px; 124 | } 125 | .lesson-editor .words-column { 126 | width: 40%; 127 | } 128 | .lesson-editor .sentences-column { 129 | width: 40%; 130 | } 131 | .lesson-editor .editor-column { 132 | width: 50%; 133 | } 134 | } 135 | 136 | @media (max-width: 700px) { 137 | .lesson-editor .words-column, 138 | .lesson-editor .sentences-column, 139 | .lesson-editor .editor-column { 140 | width: calc(100% - 48px); 141 | } 142 | 143 | } 144 | 145 | .lesson-editor .status-button { 146 | border: 1px solid #B4B3B2; 147 | color: #00A090; 148 | border-radius: 100%; 149 | background-color: #D9D8D7; 150 | width: 26px; 151 | height: 26px; 152 | text-align: center; 153 | position: absolute; 154 | right: 25px; 155 | top: 12px; 156 | } 157 | .lesson-editor .status { 158 | position: absolute; 159 | right: 3px; 160 | top: 43px; 161 | padding: 10px; 162 | max-width: 400px; 163 | background: #E8E7E6; 164 | border-radius: 10px; 165 | border: 1px solid #D9D8D7; 166 | } 167 | .lesson-editor .status .warning { 168 | color: #D96454; 169 | } 170 | .lesson-editor .status-button.warning { 171 | color: #fff; 172 | background-color: #D96454; 173 | border: 1px solid #fee; 174 | } 175 | 176 | .lesson-editor .avatar { 177 | width: 18px; 178 | vertical-align: top; 179 | } -------------------------------------------------------------------------------- /app/public/rice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 11 | 12 | 14 | 17 | 18 | 21 | 23 | 29 | 33 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/public/meat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 18 | 19 | 23 | 28 | 34 | 38 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/public/ship.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 22 | 24 | 27 | 29 | 34 | 36 | 38 | 42 | 43 | 46 | 48 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/public/helicopter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 12 | 14 | 18 | 20 | 22 | 24 | 26 | 27 | 30 | 32 | 34 | 36 | 38 | 40 | 41 | 43 | 45 | 46 | 48 | 50 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/public/morning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 11 | 13 | 16 | 19 | 21 | 23 | 26 | 29 | 30 | 33 | 36 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/public/beer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 14 | 16 | 19 | 20 | 22 | 24 | 26 | 27 | 34 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/packages/typescript-compiler/meteor-typescript/compile-service.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import ts from "typescript"; 3 | import _ from "underscore"; 4 | 5 | import logger from "./logger"; 6 | import sourceHost from "./files-source-host"; 7 | import { 8 | normalizePath, 9 | prepareSourceMap, 10 | isSourceMap, 11 | getDepsAndRefs, 12 | getRefs, 13 | createDiagnostics, 14 | getRootedPath, 15 | TsDiagnostics, 16 | } from "./ts-utils"; 17 | 18 | var babel = Npm.require("@babel/core"); 19 | 20 | export default class CompileService { 21 | constructor(serviceHost) { 22 | this.serviceHost = serviceHost; 23 | this.service = ts.createLanguageService(serviceHost); 24 | } 25 | 26 | compile(filePath, moduleName) { 27 | const sourceFile = this.getSourceFile(filePath); 28 | assert.ok(sourceFile); 29 | 30 | if (moduleName) { 31 | sourceFile.moduleName = moduleName; 32 | } 33 | 34 | const result = this.service.getEmitOutput(filePath); 35 | 36 | let code, sourceMap; 37 | result.outputFiles.forEach(file => { 38 | if (normalizePath(filePath) !== 39 | normalizePath(file.name)) return; 40 | 41 | const text = file.text; 42 | if (isSourceMap(file.name)) { 43 | const source = sourceHost.get(filePath); 44 | sourceMap = prepareSourceMap(text, source, filePath); 45 | } else { 46 | code = text; 47 | } 48 | }); 49 | 50 | if (/\.tsx$/.test(filePath)) { 51 | code = babel.transformSync(code, { presets: [ "@vue/babel-preset-jsx" ] }).code; 52 | } 53 | 54 | const checker = this.getTypeChecker(); 55 | const pcs = logger.newProfiler("process csresult"); 56 | const deps = getDepsAndRefs(sourceFile, checker); 57 | const meteorizedCode = this.rootifyPaths(code, deps.mappings); 58 | const csResult = createCSResult(filePath, { 59 | code: meteorizedCode, 60 | sourceMap, 61 | version: this.serviceHost.getScriptVersion(filePath), 62 | isExternal: ts.isExternalModule(sourceFile), 63 | dependencies: deps, 64 | diagnostics: this.getDiagnostics(filePath), 65 | }); 66 | pcs.end(); 67 | 68 | return csResult; 69 | } 70 | 71 | getHost() { 72 | return this.serviceHost; 73 | } 74 | 75 | getDocRegistry() { 76 | return this.registry; 77 | } 78 | 79 | getSourceFile(filePath) { 80 | const program = this.service.getProgram(); 81 | return program.getSourceFile(filePath); 82 | } 83 | 84 | getDepsAndRefs(filePath) { 85 | const checker = this.getTypeChecker(); 86 | return getDepsAndRefs(this.getSourceFile(filePath), checker); 87 | } 88 | 89 | getRefTypings(filePath) { 90 | const refs = getRefs(this.getSourceFile(filePath)); 91 | return refs.refTypings; 92 | } 93 | 94 | getTypeChecker() { 95 | return this.service.getProgram().getTypeChecker(); 96 | } 97 | 98 | getDiagnostics(filePath) { 99 | return createDiagnostics( 100 | this.service.getSyntacticDiagnostics(filePath), 101 | this.service.getSemanticDiagnostics(filePath) 102 | ); 103 | } 104 | 105 | rootifyPaths(code, mappings) { 106 | function buildPathRegExp(modulePath) { 107 | const regExp = new RegExp("(require\\(\"|\')(" + modulePath + ")(\"|\'\\))", "g"); 108 | return regExp; 109 | } 110 | 111 | mappings = mappings.filter(module => module.resolved && !module.external); 112 | logger.assert("process mappings %s", mappings.map((module) => module.resolvedPath)); 113 | for (const module of mappings) { 114 | const usedPath = module.modulePath; 115 | const resolvedPath = module.resolvedPath; 116 | 117 | // Fix some weird v2.1.x bug where 118 | // LanguageService converts dotted paths 119 | // to relative in the code. 120 | const regExp = buildPathRegExp(resolvedPath); 121 | code = code.replace(regExp, function(match, p1, p2, p3, offset) { 122 | return p1 + getRootedPath(resolvedPath) + p3; 123 | }); 124 | 125 | // Skip path replacement for dotted paths. 126 | if (! usedPath.startsWith(".")) { 127 | const regExp = buildPathRegExp(usedPath); 128 | code = code.replace(regExp, function(match, p1, p2, p3, offset) { 129 | return p1 + getRootedPath(resolvedPath) + p3; 130 | }); 131 | } 132 | } 133 | return code; 134 | } 135 | } 136 | 137 | export function createCSResult(filePath, result) { 138 | const props = [ 139 | "code", "sourceMap", "version", 140 | "isExternal", "dependencies", "diagnostics", 141 | ]; 142 | const len = props.length; 143 | for (let i = 0; i < len; i++) { 144 | if (!(props[i] in result)) { 145 | const msg = `file result ${filePath} doesn't contain ${props[i]}`; 146 | logger.debug(msg); 147 | throw new Error(msg); 148 | } 149 | } 150 | result.diagnostics = new TsDiagnostics( 151 | result.diagnostics); 152 | 153 | return new CSResult(result); 154 | } 155 | 156 | export class CSResult { 157 | constructor(result) { 158 | assert.ok(this instanceof CSResult); 159 | 160 | Object.assign(this, result); 161 | } 162 | 163 | upDiagnostics(diagnostics) { 164 | this.diagnostics = new TsDiagnostics(diagnostics); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/public/potato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 12 | 15 | 18 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/packages/ts-decorators/ts-decorators.js: -------------------------------------------------------------------------------- 1 | Plugin.registerCompiler({ 2 | extensions: ['ts', 'tsx'] 3 | }, function() { return new TsDecoratorsCompiler() }); 4 | 5 | var TsDecoratorsCompiler = function() { this.tsCompiler = new TypeScriptCompiler({ 6 | "lib": [ "es2015", "es2015.promise", "es2017.object", "dom" ], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "jsx": "preserve", 11 | "types": [ "meteor", "node" ], 12 | "importHelpers": true 13 | }); } 14 | 15 | TsDecoratorsCompiler.prototype.processFilesForTarget = function(filesToProcess) { 16 | 17 | var pluginFile; 18 | var rootPath; 19 | for (var file of filesToProcess) { 20 | if (!rootPath || rootPath.length > file.getSourceRoot().length) { 21 | rootPath = file.getSourceRoot(); 22 | pluginFile = file; 23 | } 24 | } 25 | 26 | if (pluginFile.getArch().indexOf("web.") == 0) { 27 | 28 | var addedJs = ""; 29 | var controllers = []; 30 | 31 | var processFile = function(path, fileName) { 32 | var contents = Plugin["fs"].readFileSync(path + "/" + fileName, { encoding: 'utf-8' }); 33 | var pos = 0; 34 | var newPos = 0; 35 | 36 | while ((newPos = contents.substring(pos).search(/@Decorators\.(method|publish)\s*(?:public\s*)?(?:static\s*)?([A-Za-z_]+)/)) > -1) { 37 | var match = contents.substring(pos).match(/@Decorators\.(method|publish)\s*(?:public\s*)?(?:static\s*)?([A-Za-z_]+)/); 38 | var decoratorType = match[1]; 39 | var methodName = match[2]; 40 | var classMatch = contents.substring(0, pos + newPos).replace(/[\r\n]/g,' ').match(/(?:^|[^a-zA-Z0-9_-])class ([A-Za-z0-9_]+)/g); 41 | var className = classMatch && classMatch[classMatch.length-1].match(/class ([A-Za-z0-9_]+)/)[1]; 42 | var clientJs = `var ${className} = this.${className} || {}; this.${className} = ${className};`; 43 | if (decoratorType == "method") 44 | clientJs += ` 45 | ${className}.${methodName} = function() { 46 | var args = Array.prototype.slice.call(arguments); 47 | return new Promise(function(resolve, reject) { 48 | args.unshift("${className}.${methodName}"); 49 | args.push(function(error, result) { if (error) reject(error); else resolve(result); }); 50 | return Meteor.call.apply(this, args); 51 | }); 52 | }\n`; 53 | else if (decoratorType == "publish") 54 | clientJs += ` 55 | ${className}.${methodName} = function() { 56 | var args = Array.prototype.slice.call(arguments); 57 | args.unshift("${className}.${methodName}"); 58 | return Meteor.subscribe.apply(this, args); 59 | }\n`; 60 | pos += newPos + 1; 61 | addedJs += clientJs; 62 | controllers.push(className); 63 | } 64 | }; 65 | 66 | var readDirRecursively = function(path) { 67 | var filesList = Plugin["fs"].readdirSync(path); 68 | if (filesList) 69 | for (var file of filesList) { 70 | if (Plugin["fs"].lstatSync(path + "/" + file).isDirectory() && file != "node_modules" && file.indexOf(".") != 0) 71 | readDirRecursively(path + "/" + file); 72 | else if (file.slice(-5) != ".d.ts" && file.slice(-3) == ".ts") 73 | processFile(path, file); 74 | } 75 | }; 76 | 77 | try { 78 | readDirRecursively(rootPath); 79 | } catch(e) { 80 | console.log("ERROR", e); 81 | } 82 | 83 | // cut out server side imports 84 | for (let file of filesToProcess) { 85 | const contents = file.getContentsAsString(); 86 | let found = false; 87 | const updated = contents.replace(/import\s*{\s*([A-Za-z0-9_]+)\s*}\sfrom\s*(?:"[^"]+"|'[^']+')/g, 88 | function(matched, name) { 89 | if (controllers.indexOf(name) > -1) { 90 | found = true; 91 | return "declare var " + name; 92 | } else { 93 | return matched; 94 | } 95 | }); 96 | 97 | if (found) { 98 | file.getContentsAsString = () => updated; 99 | } 100 | } 101 | 102 | var proxy = filesToProcess.filter(f => f.getBasename() == "Decorators_proxies.ts")[0]; 103 | proxy.addJavaScript({ data: addedJs, path: file.getBasename() }); 104 | 105 | } else { 106 | const imports = filesToProcess 107 | .filter(file => file.getPathInPackage().indexOf('imports') == 0) 108 | .filter(file => file.getContentsAsString().search(/@Decorators\.(method|publish)\s*(?:public\s*)?(?:static\s*)?([A-Za-z_]+)/) > -1) 109 | .map(file => file.getPathInPackage()) 110 | .map(i => "import '/" + i + "'").join('\n'); 111 | 112 | const importsFile = filesToProcess.filter(f => f.getBasename() == "main.ts" && f.getPathInPackage().indexOf('imports') != 0)[0]; 113 | const contents = importsFile.getContentsAsString(); 114 | importsFile.getContentsAsString = () => imports + "\n\n" + contents; 115 | } 116 | 117 | this.tsCompiler.processFilesForTarget(filesToProcess); 118 | 119 | } 120 | -------------------------------------------------------------------------------- /app/public/chicken.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 14 | 19 | 25 | 28 | 32 | 38 | 41 | 44 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /app/public/girl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 17 | 20 | 21 | 23 | 24 | 25 | 27 | 28 | 29 | 32 | 33 | 34 | 36 | 37 | 38 | 40 | 41 | 42 | 45 | 46 | 50 | 51 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | --------------------------------------------------------------------------------