├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE.md ├── app ├── App_Resources │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── app.gradle │ │ ├── drawable-hdpi │ │ │ ├── background.png │ │ │ ├── ic_add_white.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── drawable-ldpi │ │ │ ├── background.png │ │ │ ├── ic_add_white.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── drawable-mdpi │ │ │ ├── background.png │ │ │ ├── ic_add_white.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── drawable-nodpi │ │ │ └── splash_screen.xml │ │ ├── drawable-xhdpi │ │ │ ├── background.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── drawable-xxhdpi │ │ │ ├── background.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── drawable-xxxhdpi │ │ │ ├── background.png │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── logo_small.png │ │ ├── settings.gradle │ │ ├── settings.json │ │ ├── values-de │ │ │ └── strings.xml │ │ ├── values-v21 │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ └── iOS │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon-1024.png │ │ │ ├── icon-29.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-76.png │ │ │ ├── icon-76@2x.png │ │ │ └── icon-83.5@2x.png │ │ ├── Contents.json │ │ ├── LaunchImage.launchimage │ │ │ ├── Contents.json │ │ │ ├── Default-1125h.png │ │ │ ├── Default-568h@2x.png │ │ │ ├── Default-667h@2x.png │ │ │ ├── Default-736h@3x.png │ │ │ ├── Default-Landscape-X.png │ │ │ ├── Default-Landscape.png │ │ │ ├── Default-Landscape@2x.png │ │ │ ├── Default-Landscape@3x.png │ │ │ ├── Default-Portrait.png │ │ │ ├── Default-Portrait@2x.png │ │ │ ├── Default.png │ │ │ └── Default@2x.png │ │ ├── LaunchScreen.AspectFill.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchScreen-AspectFill.png │ │ │ └── LaunchScreen-AspectFill@2x.png │ │ └── LaunchScreen.Center.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchScreen-Center.png │ │ │ └── LaunchScreen-Center@2x.png │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ ├── build.xcconfig │ │ ├── de.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ │ ├── en.lproj │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ │ ├── ic_add_white.png │ │ ├── logo_small.png │ │ ├── logo_small@2x.png │ │ └── logo_small@3x.png ├── _app-common.scss ├── _app-variables.scss ├── app-routing.module.ts ├── app.android.scss ├── app.component.html ├── app.component.ts ├── app.ios.scss ├── app.module.ngfactory.d.ts ├── app.module.ts ├── fonts │ ├── FontAwesome.otf │ ├── font-awesome.css │ └── fontawesome-webfont.ttf ├── i18n │ ├── de.json │ └── en.default.json ├── main.aot.ts ├── main.ts ├── model │ ├── Article.ts │ ├── Articles.ts │ ├── Comment.ts │ ├── Profile.ts │ └── User.ts ├── module │ ├── article │ │ ├── article.module.ts │ │ ├── article.routing.ts │ │ ├── article.scss │ │ ├── edit-article.component.html │ │ ├── edit-article.component.ts │ │ ├── list-articles.component.html │ │ ├── list-articles.component.ts │ │ ├── view-article.component.html │ │ └── view-article.component.ts │ ├── comment │ │ ├── comment.module.ts │ │ ├── comment.routing.ts │ │ ├── comment.scss │ │ ├── list-comments.component.html │ │ ├── list-comments.component.ts │ │ ├── write-comment-modal.component.html │ │ └── write-comment-modal.component.ts │ ├── home │ │ ├── about-modal.component.html │ │ ├── about-modal.component.ts │ │ ├── home.component.html │ │ ├── home.component.ts │ │ ├── home.module.ts │ │ ├── home.routing.ts │ │ ├── home.scss │ │ ├── settings.component.html │ │ └── settings.component.ts │ └── user │ │ ├── edit-profile.component.html │ │ ├── edit-profile.component.ts │ │ ├── login.component.html │ │ ├── login.component.ts │ │ ├── profile.component.html │ │ ├── profile.component.ts │ │ ├── user.module.ts │ │ ├── user.routing.ts │ │ └── user.scss ├── package.json ├── service │ ├── AbstractHttpService.ts │ ├── ConduitService.ts │ ├── UserService.ts │ └── service.module.ts ├── util │ └── StatusBar.ts ├── vendor-platform.android.ts ├── vendor-platform.ios.ts └── vendor.ts ├── assets ├── feature.png ├── icon.png ├── screenshot_1.png ├── screenshot_2.png ├── screenshot_3.png └── screenshot_4.png ├── e2e ├── conduit.e2e-spec.ts ├── config │ ├── appium.capabilities.json │ └── mocha.opts ├── home.ts ├── setup.ts └── tsconfig.json ├── logo.png ├── package-lock.json ├── package.json ├── readme.md ├── references.d.ts ├── tsconfig.esm.json ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Use LF for shell scripts 2 | *.sh eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Smartphone (please complete the following information):** 24 | - Device: [e.g. iPhone6] 25 | - OS: [e.g. iOS8.1] 26 | - Browser [e.g. stock browser, safari] 27 | - Version [e.g. 22] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /bower_components 6 | /platforms 7 | /hooks 8 | /lib 9 | /debugger 10 | /dist 11 | /report 12 | 13 | /app/**/*.map 14 | /app/**/*.js 15 | /app/**/*.css 16 | 17 | **/*.log 18 | /tags 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | *.launch 25 | .settings/ 26 | .vscode/ 27 | .cloud/ 28 | 29 | # Tests 30 | /e2e/reports 31 | /e2e/**/*.js 32 | 33 | # 34 | *.bat 35 | 36 | #System Files 37 | .DS_Store 38 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Savas Ziplies 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. -------------------------------------------------------------------------------- /app/App_Resources/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/App_Resources/Android/app.gradle: -------------------------------------------------------------------------------- 1 | // Add your native dependencies here: 2 | 3 | // Uncomment to add recyclerview-v7 dependency 4 | //dependencies { 5 | // compile 'com.android.support:recyclerview-v7:+' 6 | //} 7 | 8 | android { 9 | defaultConfig { 10 | generatedDensities = [] 11 | applicationId = "com.insanitydesign.nativescriptrealworldexampleapp" 12 | } 13 | aaptOptions { 14 | additionalParameters "--no-version-vectors" 15 | } 16 | } 17 | 18 | def settingsGradlePath 19 | 20 | if(project.hasProperty("appResourcesPath")){ 21 | settingsGradlePath = "$project.appResourcesPath/Android/settings.gradle"; 22 | } else { 23 | settingsGradlePath = "$rootDir/../../app/App_Resources/Android/settings.gradle"; 24 | } 25 | 26 | def settingsGradleFile = new File(settingsGradlePath); 27 | 28 | if(settingsGradleFile.exists()) 29 | { 30 | apply from: settingsGradleFile; 31 | } 32 | -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-hdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-hdpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-hdpi/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-hdpi/ic_add_white.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-hdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-hdpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-hdpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-hdpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-ldpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-ldpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-ldpi/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-ldpi/ic_add_white.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-ldpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-ldpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-ldpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-ldpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-mdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-mdpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-mdpi/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-mdpi/ic_add_white.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-mdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-mdpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-mdpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-mdpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-nodpi/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xhdpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xhdpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xhdpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xhdpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxhdpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxhdpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxhdpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxhdpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxxhdpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxxhdpi/background.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxxhdpi/icon.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxxhdpi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxxhdpi/logo.png -------------------------------------------------------------------------------- /app/App_Resources/Android/drawable-xxxhdpi/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/Android/drawable-xxxhdpi/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/Android/settings.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonSlurper 2 | 3 | task replaceSettings { 4 | description "Replaces configuration settings." 5 | def jsonSlurper = new JsonSlurper() 6 | def pathToSettingsJson 7 | 8 | if (project.hasProperty("appResourcesPath")) { 9 | pathToSettingsJson = "$project.appResourcesPath/Android/settings.json"; 10 | } else { 11 | pathToSettingsJson = "$rootDir/../../app/App_Resources/Android/settings.json"; 12 | } 13 | 14 | def settingsJsonFile = file(pathToSettingsJson); 15 | def settingsResolvedPath = settingsJsonFile.getAbsolutePath(); 16 | 17 | if(settingsJsonFile.exists()) 18 | { 19 | println "\t Applying settings from $settingsResolvedPath" 20 | String settingsGradleTemplate = """android { 21 | defaultConfig { 22 | applicationId = "__appId__" 23 | 24 | if (__minSdkVersion__) { 25 | minSdkVersion = __minSdkVersion__ 26 | } 27 | 28 | if (__targetSdkVersion__) { 29 | targetSdkVersion = __targetSdkVersion__ 30 | } 31 | } 32 | }""" 33 | 34 | def settingsJsonContent = settingsJsonFile.getText("UTF-8"); 35 | def settingsMap = jsonSlurper.parseText(settingsJsonContent); 36 | 37 | for ( setting in settingsMap ) { 38 | def placeholder = "__${setting.key}__"; 39 | def settingValue = setting.value; 40 | 41 | if (settingValue == null) { 42 | settingValue = false 43 | } 44 | 45 | settingsGradleTemplate = settingsGradleTemplate.replaceAll( placeholder, settingValue as String); 46 | } 47 | 48 | new File( "$rootDir/temp_setting.gradle" ).write( settingsGradleTemplate, 'UTF-8'); 49 | apply from: "$rootDir/temp_setting.gradle"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/App_Resources/Android/settings.json: -------------------------------------------------------------------------------- 1 | {"appId":"com.insanitydesign.nativescriptrealworldexampleapp","minSdkVersion":23,"targetSdkVersion":null} -------------------------------------------------------------------------------- /app/App_Resources/Android/values-de/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | "Das darf nicht leer sein!" 4 | "Bitte einloggen!" 5 | "Ein Fehler ist aufgetreten!" 6 | "Möchtest du diesen Kommentar wirklich löschen?" 7 | "Veröffentlichen" 8 | "Reaktion auf" 9 | "Schreib einen Kommentar..." 10 | "Keine Kommentare!" 11 | "Favorisierte Beiträge" 12 | "Meine Beiträge" 13 | "Entfolgen" 14 | "Folgen" 15 | "Profil aktualisiert!" 16 | "Bitte einen Nutzernamen angeben!" 17 | "Bitte gib eine korrekte E-Mail Adresse ein!" 18 | "Bitte die gleichen Passwörter eingeben!" 19 | "Bitte alle geforderten Felder füllen!" 20 | "Noch kein Account?" 21 | "Zurück zu Home" 22 | "Zurück zum Login" 23 | "Bild" 24 | "Bio" 25 | "Passwort bestätigen" 26 | "Passwort" 27 | "E-Mail" 28 | "Nutzername" 29 | "Registrieren" 30 | "Logout" 31 | "Login" 32 | "Beitrag gelöscht!" 33 | "Beitrag gespeichert!" 34 | "Möchtest du diesen Betrag wirklich löschen?" 35 | "Tags" 36 | "Inhalt" 37 | "Beschreibung" 38 | "Titel" 39 | "Keine Artikel gefunden!" 40 | "Beitrag löschen" 41 | "Beitrag bearbeiten" 42 | "Beitrag schreiben" 43 | "Beiträge geladen" 44 | "Lade Beiträge..." 45 | "Globale Artikel" 46 | "Deine Artikel" 47 | "Du wurdest erfolgreich ausgeloggt!" 48 | "Erstellt von https://github.com/nea/" 49 | "Über" 50 | "Einstellungen" 51 | "Home" 52 | "Logout" 53 | "Login/Registrieren" 54 | "Zurücksetzen" 55 | "Bitte eine korrekt URL eingeben" 56 | "Eine URL, welche die Conduit Spec implementiert" 57 | "Backend URL" 58 | "Einstellungen" 59 | "Dieses Beispielprojekt wude durch INsanityDesign.com für RealWorld.io erstellt und ist lizensiert unter der MIT. Du findest den Source auf GitHub.com." 60 | "RealWorld Beispielprojekte wurden durch Thinkster.io gestartet um echte Szenarien als Tutorials anzubieten um zu zeigen, wie verschiedene Backend/Frontend Programmiersprachen und/oder Frameworks in deinem Projekt funktionieren könnten." 61 | "Diese App ist ein Beispielprojekt eines Medium.com Klon (genannt Conduit) gebaut in NativeScript, welcher mit verschiedenen Backends verbunden werden kann von " 62 | "schließen" 63 | "Speichern" 64 | "Eine NativeScript RealWorld Beispiel App" 65 | "Conduit" 66 | "Conduit" 67 | "Conduit" 68 | "Conduit" 69 | 70 | -------------------------------------------------------------------------------- /app/App_Resources/Android/values-v21/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3d5afe 4 | -------------------------------------------------------------------------------- /app/App_Resources/Android/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 14 | 15 | 16 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /app/App_Resources/Android/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F5F5F5 4 | #757575 5 | #33B5E5 6 | #272734 7 | -------------------------------------------------------------------------------- /app/App_Resources/Android/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | "This may not be empty!" 4 | "Please login!" 5 | "An error occured!" 6 | "Are you sure you want to delete this comment?" 7 | "Publish" 8 | "In response to" 9 | "Write a response..." 10 | "No comments!" 11 | "Favorited Articles" 12 | "My Articles" 13 | "Unfollow" 14 | "Follow" 15 | "Profile updated!" 16 | "Please provide a username!" 17 | "Please enter a valid Email!" 18 | "Please provide matching passwords!" 19 | "Please provide all requested inputs!" 20 | "Don\'t have an account?" 21 | "Back to home" 22 | "Back to login" 23 | "Image" 24 | "Bio" 25 | "Confirm password" 26 | "Password" 27 | "Email" 28 | "Username" 29 | "Register" 30 | "Logout" 31 | "Login" 32 | "Article deleted!" 33 | "Article saved!" 34 | "Are you sure you want to delete this article?" 35 | "Tags" 36 | "Body" 37 | "Description" 38 | "Title" 39 | "No articles found!" 40 | "Delete article" 41 | "Edit article" 42 | "Add article" 43 | "Loaded articles" 44 | "Loading articles..." 45 | "Global Feed" 46 | "Your Feed" 47 | "You have been logged out successfully!" 48 | "Created by https://github.com/nea/" 49 | "About" 50 | "Settings" 51 | "Home" 52 | "Logout" 53 | "Login/Register" 54 | "Reset" 55 | "Please enter a valid URL" 56 | "URL implementing the Conduit backend spec" 57 | "Backend URL" 58 | "Settings" 59 | "This example stack has been created by INsanityDesign.com for RealWorld.io and is licensed under MIT. You can checkout the full source at GitHub.com." 60 | "RealWorld example projects have been initiated by Thinkster.io to provide a real-world scenario as tutorial to see how any different backend/frontend programming language and/or framework could work out in your project." 61 | "This app is an exemplary project to show how a Medium.com clone (called Conduit) is built using NativeScript to connect to any other backend from " 62 | "Close" 63 | "Save" 64 | "A NativeScript RealWorld Example App" 65 | "Conduit" 66 | "Conduit" 67 | "Conduit" 68 | "Conduit" 69 | 70 | -------------------------------------------------------------------------------- /app/App_Resources/Android/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 20 | 21 | 22 | 29 | 30 | 32 | 33 | 34 | 39 | 40 | 42 | 43 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "icon-29.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "icon-29@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon-29@3x.png", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "icon-40@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon-40@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "icon-60@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-60@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "29x29", 47 | "idiom" : "ipad", 48 | "filename" : "icon-29.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "29x29", 53 | "idiom" : "ipad", 54 | "filename" : "icon-29@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "40x40", 59 | "idiom" : "ipad", 60 | "filename" : "icon-40.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "40x40", 65 | "idiom" : "ipad", 66 | "filename" : "icon-40@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "76x76", 71 | "idiom" : "ipad", 72 | "filename" : "icon-76.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "76x76", 77 | "idiom" : "ipad", 78 | "filename" : "icon-76@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "83.5x83.5", 83 | "idiom" : "ipad", 84 | "filename" : "icon-83.5@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "1024x1024", 89 | "idiom" : "ios-marketing", 90 | "filename" : "icon-1024.png", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "2436h", 7 | "filename" : "Default-1125h.png", 8 | "minimum-system-version" : "11.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "orientation" : "landscape", 14 | "idiom" : "iphone", 15 | "extent" : "full-screen", 16 | "filename" : "Default-Landscape-X.png", 17 | "minimum-system-version" : "11.0", 18 | "subtype" : "2436h", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "extent" : "full-screen", 23 | "idiom" : "iphone", 24 | "subtype" : "736h", 25 | "filename" : "Default-736h@3x.png", 26 | "minimum-system-version" : "8.0", 27 | "orientation" : "portrait", 28 | "scale" : "3x" 29 | }, 30 | { 31 | "extent" : "full-screen", 32 | "idiom" : "iphone", 33 | "subtype" : "736h", 34 | "filename" : "Default-Landscape@3x.png", 35 | "minimum-system-version" : "8.0", 36 | "orientation" : "landscape", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "extent" : "full-screen", 41 | "idiom" : "iphone", 42 | "subtype" : "667h", 43 | "filename" : "Default-667h@2x.png", 44 | "minimum-system-version" : "8.0", 45 | "orientation" : "portrait", 46 | "scale" : "2x" 47 | }, 48 | { 49 | "orientation" : "portrait", 50 | "idiom" : "iphone", 51 | "filename" : "Default@2x.png", 52 | "extent" : "full-screen", 53 | "minimum-system-version" : "7.0", 54 | "scale" : "2x" 55 | }, 56 | { 57 | "extent" : "full-screen", 58 | "idiom" : "iphone", 59 | "subtype" : "retina4", 60 | "filename" : "Default-568h@2x.png", 61 | "minimum-system-version" : "7.0", 62 | "orientation" : "portrait", 63 | "scale" : "2x" 64 | }, 65 | { 66 | "orientation" : "portrait", 67 | "idiom" : "ipad", 68 | "filename" : "Default-Portrait.png", 69 | "extent" : "full-screen", 70 | "minimum-system-version" : "7.0", 71 | "scale" : "1x" 72 | }, 73 | { 74 | "orientation" : "landscape", 75 | "idiom" : "ipad", 76 | "filename" : "Default-Landscape.png", 77 | "extent" : "full-screen", 78 | "minimum-system-version" : "7.0", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "orientation" : "portrait", 83 | "idiom" : "ipad", 84 | "filename" : "Default-Portrait@2x.png", 85 | "extent" : "full-screen", 86 | "minimum-system-version" : "7.0", 87 | "scale" : "2x" 88 | }, 89 | { 90 | "orientation" : "landscape", 91 | "idiom" : "ipad", 92 | "filename" : "Default-Landscape@2x.png", 93 | "extent" : "full-screen", 94 | "minimum-system-version" : "7.0", 95 | "scale" : "2x" 96 | }, 97 | { 98 | "orientation" : "portrait", 99 | "idiom" : "iphone", 100 | "filename" : "Default.png", 101 | "extent" : "full-screen", 102 | "scale" : "1x" 103 | }, 104 | { 105 | "orientation" : "portrait", 106 | "idiom" : "iphone", 107 | "filename" : "Default@2x.png", 108 | "extent" : "full-screen", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "orientation" : "portrait", 113 | "idiom" : "iphone", 114 | "filename" : "Default-568h@2x.png", 115 | "extent" : "full-screen", 116 | "subtype" : "retina4", 117 | "scale" : "2x" 118 | }, 119 | { 120 | "orientation" : "portrait", 121 | "idiom" : "ipad", 122 | "extent" : "to-status-bar", 123 | "scale" : "1x" 124 | }, 125 | { 126 | "orientation" : "portrait", 127 | "idiom" : "ipad", 128 | "filename" : "Default-Portrait.png", 129 | "extent" : "full-screen", 130 | "scale" : "1x" 131 | }, 132 | { 133 | "orientation" : "landscape", 134 | "idiom" : "ipad", 135 | "extent" : "to-status-bar", 136 | "scale" : "1x" 137 | }, 138 | { 139 | "orientation" : "landscape", 140 | "idiom" : "ipad", 141 | "filename" : "Default-Landscape.png", 142 | "extent" : "full-screen", 143 | "scale" : "1x" 144 | }, 145 | { 146 | "orientation" : "portrait", 147 | "idiom" : "ipad", 148 | "extent" : "to-status-bar", 149 | "scale" : "2x" 150 | }, 151 | { 152 | "orientation" : "portrait", 153 | "idiom" : "ipad", 154 | "filename" : "Default-Portrait@2x.png", 155 | "extent" : "full-screen", 156 | "scale" : "2x" 157 | }, 158 | { 159 | "orientation" : "landscape", 160 | "idiom" : "ipad", 161 | "extent" : "to-status-bar", 162 | "scale" : "2x" 163 | }, 164 | { 165 | "orientation" : "landscape", 166 | "idiom" : "ipad", 167 | "filename" : "Default-Landscape@2x.png", 168 | "extent" : "full-screen", 169 | "scale" : "2x" 170 | } 171 | ], 172 | "info" : { 173 | "version" : 1, 174 | "author" : "xcode" 175 | } 176 | } -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-1125h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-1125h.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape-X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape-X.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchScreen-AspectFill.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchScreen-AspectFill@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchScreen-Center.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchScreen-Center@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Conduit 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Conduit 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.1.1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiresFullScreen 28 | 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/build.xcconfig: -------------------------------------------------------------------------------- 1 | // You can add custom settings here 2 | // for example you can uncomment the following line to force distribution code signing 3 | // CODE_SIGN_IDENTITY = iPhone Distribution 4 | // To build for device with XCode 8 you need to specify your development team. 5 | // DEVELOPMENT_TEAM = YOUR_TEAM_ID 6 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 7 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; 8 | TARGETED_DEVICE_FAMILY = 1,2; 9 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/de.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Conduit"; 2 | "CFBundleName" = "Conduit"; 3 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "_validation_notEmpty_1nAY96" = "Das darf nicht leer sein!"; 2 | "_error_login_2ri64q" = "Bitte einloggen!"; 3 | "_error_general_29Dwqq" = "Ein Fehler ist aufgetreten!"; 4 | "_comment_delete_confirm_Z1NRVEk" = "Möchtest du diesen Kommentar wirklich löschen?"; 5 | "_comment_publish_5EfXd" = "Veröffentlichen"; 6 | "_comment_inResponse_Z8mVuK" = "Reaktion auf"; 7 | "_comment_response_Vrx3w" = "Schreib einen Kommentar..."; 8 | "_comment_empty_262VIT" = "Keine Kommentare!"; 9 | "_user_feed_favorited_2txVxh" = "Favorisierte Beiträge"; 10 | "_user_feed_user_15fqdE" = "Meine Beiträge"; 11 | "_user_unfollow_Z2jCfUV" = "Entfolgen"; 12 | "_user_follow_Z1fU5gH" = "Folgen"; 13 | "_user_form_saved_Z2iuyuC" = "Profil aktualisiert!"; 14 | "_user_form_error_validUsername_2j4WBp" = "Bitte einen Nutzernamen angeben!"; 15 | "_user_form_error_validEmail_1kuQBh" = "Bitte gib eine korrekte E-Mail Adresse ein!"; 16 | "_user_form_error_passwordMismatch_Z1KPdW9" = "Bitte die gleichen Passwörter eingeben!"; 17 | "_user_form_error_missing_Z1xz1qv" = "Bitte alle geforderten Felder füllen!"; 18 | "_user_form_needAccount_ZBcBvf" = "Noch kein Account?"; 19 | "_user_form_backToHome_Z11n6D6" = "Zurück zu Home"; 20 | "_user_form_backToLogin_Z1bhfFn" = "Zurück zum Login"; 21 | "_user_form_image_Z2j8KJM" = "Bild"; 22 | "_user_form_bio_Z1iRqqr" = "Bio"; 23 | "_user_form_passwordConfirm_Z17xtkM" = "Passwort bestätigen"; 24 | "_user_form_password_1plOy7" = "Passwort"; 25 | "_user_form_email_Z2jp2up" = "E-Mail"; 26 | "_user_form_username_ZkIqne" = "Nutzername"; 27 | "_user_register_2ol0nM" = "Registrieren"; 28 | "_user_logout_Z13vVPg" = "Logout"; 29 | "_user_login_BVAAY" = "Login"; 30 | "_article_form_deleted_UwfYY" = "Beitrag gelöscht!"; 31 | "_article_form_saved_ZbwLkB" = "Beitrag gespeichert!"; 32 | "_article_form_confirmDelete_25feI5" = "Möchtest du diesen Betrag wirklich löschen?"; 33 | "_article_form_tags_ZunJE7" = "Tags"; 34 | "_article_form_body_Zuq3aL" = "Inhalt"; 35 | "_article_form_description_Z32jJl" = "Beschreibung"; 36 | "_article_form_title_ZbrExP" = "Titel"; 37 | "_article_empty_ZSs3Sg" = "Keine Artikel gefunden!"; 38 | "_article_delete_Z2hRvR1" = "Beitrag löschen"; 39 | "_article_edit_ZvLn2r" = "Beitrag bearbeiten"; 40 | "_article_add_X0N7q" = "Beitrag schreiben"; 41 | "_article_loaded_Z20GeRU" = "Beiträge geladen"; 42 | "_article_loading_Z1k4Nqs" = "Lade Beiträge..."; 43 | "_article_feed_global_ea326" = "Globale Artikel"; 44 | "_article_feed_user_unCrd" = "Deine Artikel"; 45 | "_drawer_feedback_loggedOut_1JaKRB" = "Du wurdest erfolgreich ausgeloggt!"; 46 | "_drawer_createdBy_Ze1Mjk" = "Erstellt von https://github.com/nea/"; 47 | "_drawer_about_Z1I6Qod" = "Über"; 48 | "_drawer_settings_FIu8R" = "Einstellungen"; 49 | "_drawer_home_gCUhF" = "Home"; 50 | "_drawer_logout_Z2aSUPE" = "Logout"; 51 | "_drawer_login_Z1HlqmX" = "Login/Registrieren"; 52 | "_settings_reset_1zRWpY" = "Zurücksetzen"; 53 | "_settings_urlWarning_ZlnNDI" = "Bitte eine korrekt URL eingeben"; 54 | "_settings_urlHint_ZJdzNL" = "Eine URL, welche die Conduit Spec implementiert"; 55 | "_settings_url_Cvh8y" = "Backend URL"; 56 | "_settings_title_1A1C8V" = "Einstellungen"; 57 | "_about_insanitydesign_2vmxt9" = "Dieses Beispielprojekt wude durch INsanityDesign.com für RealWorld.io erstellt und ist lizensiert unter der MIT. Du findest den Source auf GitHub.com."; 58 | "_about_thinkster_Z1Mxmg9" = "RealWorld Beispielprojekte wurden durch Thinkster.io gestartet um echte Szenarien als Tutorials anzubieten um zu zeigen, wie verschiedene Backend/Frontend Programmiersprachen und/oder Frameworks in deinem Projekt funktionieren könnten."; 59 | "_about_realworld_Z1YNVKo" = "Diese App ist ein Beispielprojekt eines Medium.com Klon (genannt Conduit) gebaut in NativeScript, welcher mit verschiedenen Backends verbunden werden kann von "; 60 | "_general_close_1X64mW" = "schließen"; 61 | "_general_save_1GXi6l" = "Speichern"; 62 | "_description_Z22xK3K" = "Eine NativeScript RealWorld Beispiel App"; 63 | "_title_7XfKt" = "Conduit"; 64 | "_app_name_1k3Sbz" = "Conduit"; 65 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Conduit"; 2 | "CFBundleName" = "Conduit"; 3 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "_validation_notEmpty_1nAY96" = "This may not be empty!"; 2 | "_error_login_2ri64q" = "Please login!"; 3 | "_error_general_29Dwqq" = "An error occured!"; 4 | "_comment_delete_confirm_Z1NRVEk" = "Are you sure you want to delete this comment?"; 5 | "_comment_publish_5EfXd" = "Publish"; 6 | "_comment_inResponse_Z8mVuK" = "In response to"; 7 | "_comment_response_Vrx3w" = "Write a response..."; 8 | "_comment_empty_262VIT" = "No comments!"; 9 | "_user_feed_favorited_2txVxh" = "Favorited Articles"; 10 | "_user_feed_user_15fqdE" = "My Articles"; 11 | "_user_unfollow_Z2jCfUV" = "Unfollow"; 12 | "_user_follow_Z1fU5gH" = "Follow"; 13 | "_user_form_saved_Z2iuyuC" = "Profile updated!"; 14 | "_user_form_error_validUsername_2j4WBp" = "Please provide a username!"; 15 | "_user_form_error_validEmail_1kuQBh" = "Please enter a valid Email!"; 16 | "_user_form_error_passwordMismatch_Z1KPdW9" = "Please provide matching passwords!"; 17 | "_user_form_error_missing_Z1xz1qv" = "Please provide all requested inputs!"; 18 | "_user_form_needAccount_ZBcBvf" = "Don't have an account?"; 19 | "_user_form_backToHome_Z11n6D6" = "Back to home"; 20 | "_user_form_backToLogin_Z1bhfFn" = "Back to login"; 21 | "_user_form_image_Z2j8KJM" = "Image"; 22 | "_user_form_bio_Z1iRqqr" = "Bio"; 23 | "_user_form_passwordConfirm_Z17xtkM" = "Confirm password"; 24 | "_user_form_password_1plOy7" = "Password"; 25 | "_user_form_email_Z2jp2up" = "Email"; 26 | "_user_form_username_ZkIqne" = "Username"; 27 | "_user_register_2ol0nM" = "Register"; 28 | "_user_logout_Z13vVPg" = "Logout"; 29 | "_user_login_BVAAY" = "Login"; 30 | "_article_form_deleted_UwfYY" = "Article deleted!"; 31 | "_article_form_saved_ZbwLkB" = "Article saved!"; 32 | "_article_form_confirmDelete_25feI5" = "Are you sure you want to delete this article?"; 33 | "_article_form_tags_ZunJE7" = "Tags"; 34 | "_article_form_body_Zuq3aL" = "Body"; 35 | "_article_form_description_Z32jJl" = "Description"; 36 | "_article_form_title_ZbrExP" = "Title"; 37 | "_article_empty_ZSs3Sg" = "No articles found!"; 38 | "_article_delete_Z2hRvR1" = "Delete article"; 39 | "_article_edit_ZvLn2r" = "Edit article"; 40 | "_article_add_X0N7q" = "Add article"; 41 | "_article_loaded_Z20GeRU" = "Loaded articles"; 42 | "_article_loading_Z1k4Nqs" = "Loading articles..."; 43 | "_article_feed_global_ea326" = "Global Feed"; 44 | "_article_feed_user_unCrd" = "Your Feed"; 45 | "_drawer_feedback_loggedOut_1JaKRB" = "You have been logged out successfully!"; 46 | "_drawer_createdBy_Ze1Mjk" = "Created by https://github.com/nea/"; 47 | "_drawer_about_Z1I6Qod" = "About"; 48 | "_drawer_settings_FIu8R" = "Settings"; 49 | "_drawer_home_gCUhF" = "Home"; 50 | "_drawer_logout_Z2aSUPE" = "Logout"; 51 | "_drawer_login_Z1HlqmX" = "Login/Register"; 52 | "_settings_reset_1zRWpY" = "Reset"; 53 | "_settings_urlWarning_ZlnNDI" = "Please enter a valid URL"; 54 | "_settings_urlHint_ZJdzNL" = "URL implementing the Conduit backend spec"; 55 | "_settings_url_Cvh8y" = "Backend URL"; 56 | "_settings_title_1A1C8V" = "Settings"; 57 | "_about_insanitydesign_2vmxt9" = "This example stack has been created by INsanityDesign.com for RealWorld.io and is licensed under MIT. You can checkout the full source at GitHub.com."; 58 | "_about_thinkster_Z1Mxmg9" = "RealWorld example projects have been initiated by Thinkster.io to provide a real-world scenario as tutorial to see how any different backend/frontend programming language and/or framework could work out in your project."; 59 | "_about_realworld_Z1YNVKo" = "This app is an exemplary project to show how a Medium.com clone (called Conduit) is built using NativeScript to connect to any other backend from "; 60 | "_general_close_1X64mW" = "Close"; 61 | "_general_save_1GXi6l" = "Save"; 62 | "_description_Z22xK3K" = "A NativeScript RealWorld Example App"; 63 | "_title_7XfKt" = "Conduit"; 64 | "_app_name_1k3Sbz" = "Conduit"; 65 | -------------------------------------------------------------------------------- /app/App_Resources/iOS/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/ic_add_white.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/logo_small.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/logo_small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/logo_small@2x.png -------------------------------------------------------------------------------- /app/App_Resources/iOS/logo_small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/App_Resources/iOS/logo_small@3x.png -------------------------------------------------------------------------------- /app/_app-common.scss: -------------------------------------------------------------------------------- 1 | // Import app variables 2 | @import 'app-variables'; 3 | // Import the theme’s main ruleset. 4 | @import '~nativescript-theme-core/scss/index'; 5 | // Place any CSS rules you want to apply on both iOS and Android here. 6 | // This is where the vast majority of your CSS code goes. 7 | .fa { 8 | font-family: FontAwesome, fontawesome-webfont; 9 | } 10 | 11 | :disabled { 12 | opacity: 0.5; 13 | } 14 | 15 | ActionBar { 16 | background-color: $brand-primary; 17 | color: white; 18 | } 19 | 20 | ActionBar .h2 { 21 | color: white; 22 | } 23 | 24 | ActionBar .fa { 25 | font-size: 20; 26 | } 27 | 28 | ActionBar .btn-rounded { 29 | border-color: $brand-primary; 30 | border-width: 1; 31 | border-radius: 5; 32 | padding: 5; 33 | } 34 | 35 | .btn-danger { 36 | @extend .btn-primary; 37 | color: black; 38 | background-color: red; 39 | } 40 | 41 | // Drawer navigation custom styles 42 | $sidedrawer-header-image-size: 60; 43 | $sidedrawer-header-image-offset-top: 20; 44 | $sidedrawer-header-image-offset-bottom: 5; 45 | $sidedrawer-list-item-offset-left: 15; 46 | $sidedrawer-list-icon-offset: 10; 47 | $sidedrawer-list-icon-size: 20; 48 | 49 | .sidedrawer { 50 | &.sidedrawer-left { 51 | background-color: $ab-background; 52 | 53 | .sidedrawer-header-image { 54 | color: $brand-dark; 55 | height: $sidedrawer-header-image-size; 56 | width: $sidedrawer-header-image-size; 57 | font-size: $sidedrawer-header-image-size; 58 | padding: 0; 59 | margin-bottom: $sidedrawer-header-image-offset-bottom; 60 | margin-top: $sidedrawer-header-image-offset-top; 61 | } 62 | 63 | .footnote { 64 | color: rgba($ab-color, 0.5); 65 | } 66 | } 67 | 68 | .sidedrawer-header { 69 | background-color: $brand-light; 70 | 71 | .sidedrawer-header-brand { 72 | color: $ab-color; 73 | } 74 | } 75 | 76 | .sidedrawer-content { 77 | background-color: $brand-light; 78 | $sidedrawer-list-item-offset-left: 15; 79 | $sidedrawer-list-icon-offset: 10; 80 | $sidedrawer-list-icon-size: 20; 81 | 82 | .sidedrawer-list-item { 83 | padding-left: $sidedrawer-list-item-offset-left; 84 | 85 | Label { 86 | vertical-align: center; 87 | color: $brand-primary; 88 | } 89 | 90 | .fa { 91 | width: $sidedrawer-list-icon-size; 92 | margin-right: $sidedrawer-list-icon-offset; 93 | } 94 | 95 | &.selected { 96 | background-color: $brand-light; 97 | Label { 98 | color: $brand-light-font; 99 | } 100 | } 101 | } 102 | } 103 | 104 | .created-by { 105 | background-color: $brand-primary; 106 | color: $brand-primary-font; 107 | font-style: italic; 108 | font-size: 12; 109 | } 110 | } -------------------------------------------------------------------------------- /app/_app-variables.scss: -------------------------------------------------------------------------------- 1 | // Import the theme’s variables. If you’re using a color scheme 2 | // other than “light”, switch the path to the alternative scheme, 3 | // for example '~nativescript-theme-core/scss/dark'. 4 | @import '~nativescript-theme-core/scss/light'; 5 | 6 | // Customize any of the theme’s variables here, for instance $btn-color: red; 7 | $brand-primary: #5cb85c; 8 | $brand-light: #f3f3f3; 9 | $brand-dark: #818a91; 10 | 11 | $brand-primary-font: white; 12 | $brand-light-font: black; 13 | $brand-dark-font: white; 14 | 15 | $accent: $brand-primary; -------------------------------------------------------------------------------- /app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { Routes } from "@angular/router"; 3 | import { NativeScriptRouterModule } from "nativescript-angular/router"; 4 | 5 | const routes: Routes = [ 6 | { path: "", redirectTo: "/home", pathMatch: "full" }, 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [NativeScriptRouterModule.forRoot(routes)], 11 | exports: [NativeScriptRouterModule] 12 | }) 13 | export class AppRoutingModule { } 14 | -------------------------------------------------------------------------------- /app/app.android.scss: -------------------------------------------------------------------------------- 1 | @import 'app-common'; 2 | @import '~nativescript-theme-core/scss/platforms/index.android'; 3 | 4 | // Place any CSS rules you want to apply only on Android here -------------------------------------------------------------------------------- /app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | -------------------------------------------------------------------------------- /app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewContainerRef } from "@angular/core"; 2 | import { filter } from "rxjs/operators"; 3 | import { RouterExtensions } from "nativescript-angular/router"; 4 | import { Router, NavigationEnd } from "@angular/router"; 5 | import { RadSideDrawer } from "nativescript-ui-sidedrawer"; 6 | import * as app from "application"; 7 | import { UserService } from "~/service/UserService"; 8 | import { Feedback } from "nativescript-feedback"; 9 | import { localize } from "nativescript-localize"; 10 | import { ModalDialogService } from "nativescript-angular/directives/dialogs"; 11 | import { AboutModal } from "~/module/home/about-modal.component"; 12 | import * as utils from "utils/utils"; 13 | 14 | @Component({ 15 | selector: "conduit-app", 16 | templateUrl: "app.component.html", 17 | providers: [ModalDialogService] 18 | }) 19 | export class AppComponent { 20 | /** */ 21 | protected activatedUrl: string; 22 | /** */ 23 | protected feedback: Feedback; 24 | 25 | /** 26 | * 27 | * @param router 28 | * @param routerExtensions 29 | * @param modal 30 | * @param vcRef 31 | * @param userService 32 | */ 33 | constructor( 34 | protected router: Router, 35 | protected routerExtensions: RouterExtensions, 36 | protected modal: ModalDialogService, 37 | protected vcRef: ViewContainerRef, 38 | public userService: UserService 39 | ) { 40 | this.feedback = new Feedback(); 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | public ngOnInit() { 47 | this.activatedUrl = "/home"; 48 | this.router.events 49 | .pipe(filter((event: any) => event instanceof NavigationEnd)) 50 | .subscribe((event: NavigationEnd) => (this.activatedUrl = event.urlAfterRedirects)); 51 | } 52 | 53 | /** 54 | * 55 | */ 56 | public onLogout() { 57 | if (this.userService.logout()) { 58 | this.feedback.success({ 59 | title: localize("drawer.logout"), 60 | message: localize("drawer.feedback.loggedOut") 61 | }); 62 | } 63 | } 64 | 65 | /** 66 | * 67 | * @param url 68 | */ 69 | public isSelected(url: string): boolean { 70 | return this.activatedUrl === url; 71 | } 72 | 73 | /** 74 | * 75 | * @param route 76 | */ 77 | public onItem(route: string): void { 78 | this.routerExtensions.navigate([route], { 79 | transition: { 80 | name: "fade" 81 | } 82 | }); 83 | 84 | const sideDrawer = app.getRootView(); 85 | sideDrawer.closeDrawer(); 86 | } 87 | 88 | /** 89 | * Show the "About" modal 90 | */ 91 | public onAbout() { 92 | this.modal.showModal(AboutModal, { 93 | fullscreen: false, 94 | viewContainerRef: this.vcRef 95 | }); 96 | } 97 | 98 | /** 99 | * 100 | */ 101 | public onCreated() { 102 | utils.openUrl("https://github.com/nea/"); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/app.ios.scss: -------------------------------------------------------------------------------- 1 | @import 'app-common'; 2 | @import '~nativescript-theme-core/scss/platforms/index.ios'; 3 | 4 | // Place any CSS rules you want to apply only on iOS here -------------------------------------------------------------------------------- /app/app.module.ngfactory.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A dynamically generated module when compiled with AoT. 3 | */ 4 | export const AppModuleNgFactory: any; 5 | -------------------------------------------------------------------------------- /app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NgModuleFactoryLoader, NO_ERRORS_SCHEMA } from "@angular/core"; 2 | import { NativeScriptModule } from "nativescript-angular/nativescript.module"; 3 | import { NativeScriptHttpClientModule } from "nativescript-angular/http-client"; 4 | import { NativeScriptUISideDrawerModule } from "nativescript-ui-sidedrawer/angular"; 5 | import { NativeScriptLocalizeModule } from "nativescript-localize/angular"; 6 | import { NativeScriptRouterModule } from "nativescript-angular/router"; 7 | import { TNSFontIconModule } from "nativescript-ngx-fonticon"; 8 | 9 | import { AppRoutingModule } from "./app-routing.module"; 10 | import { AppComponent } from "./app.component"; 11 | 12 | import { HomeModule } from "./module/home/home.module"; 13 | import { ServiceModule } from "./service/service.module"; 14 | 15 | @NgModule({ 16 | bootstrap: [AppComponent], 17 | imports: [ 18 | NativeScriptModule, 19 | NativeScriptRouterModule, 20 | NativeScriptHttpClientModule, 21 | NativeScriptUISideDrawerModule, 22 | NativeScriptLocalizeModule, 23 | AppRoutingModule, 24 | TNSFontIconModule.forRoot({ 25 | fa: "./fonts/font-awesome.css" 26 | }), 27 | ServiceModule.forRoot(), 28 | HomeModule 29 | ], 30 | declarations: [AppComponent], 31 | schemas: [NO_ERRORS_SCHEMA] 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /app/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/app/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "Conduit", 3 | "title": "Conduit", 4 | "description": "Eine NativeScript RealWorld Beispiel App", 5 | "general": { 6 | "save": "Speichern", 7 | "close": "schließen" 8 | }, 9 | "about": { 10 | "realworld": "Diese App ist ein Beispielprojekt eines Medium.com Klon (genannt Conduit) gebaut in NativeScript, welcher mit verschiedenen Backends verbunden werden kann von ", 11 | "thinkster": "RealWorld Beispielprojekte wurden durch Thinkster.io gestartet um echte Szenarien als Tutorials anzubieten um zu zeigen, wie verschiedene Backend/Frontend Programmiersprachen und/oder Frameworks in deinem Projekt funktionieren könnten.", 12 | "insanitydesign": "Dieses Beispielprojekt wude durch INsanityDesign.com für RealWorld.io erstellt und ist lizensiert unter der MIT. Du findest den Source auf GitHub.com." 13 | }, 14 | "settings": { 15 | "title": "Einstellungen", 16 | "url": "Backend URL", 17 | "urlHint": "Eine URL, welche die Conduit Spec implementiert", 18 | "urlWarning": "Bitte eine korrekt URL eingeben", 19 | "reset": "Zurücksetzen" 20 | }, 21 | "drawer": { 22 | "login": "Login/Registrieren", 23 | "logout": "Logout", 24 | "home": "Home", 25 | "settings": "Einstellungen", 26 | "about": "Über", 27 | "createdBy": "Erstellt von https://github.com/nea/", 28 | "feedback": { 29 | "loggedOut": "Du wurdest erfolgreich ausgeloggt!" 30 | } 31 | }, 32 | "article": { 33 | "feed": { 34 | "user": "Deine Artikel", 35 | "global": "Globale Artikel" 36 | }, 37 | "loading": "Lade Beiträge...", 38 | "loaded": "Beiträge geladen", 39 | "add": "Beitrag schreiben", 40 | "edit": "Beitrag bearbeiten", 41 | "delete": "Beitrag löschen", 42 | "empty": "Keine Artikel gefunden!", 43 | "form": { 44 | "title": "Titel", 45 | "description": "Beschreibung", 46 | "body": "Inhalt", 47 | "tags": "Tags", 48 | "confirmDelete": "Möchtest du diesen Betrag wirklich löschen?", 49 | "saved": "Beitrag gespeichert!", 50 | "deleted": "Beitrag gelöscht!" 51 | } 52 | }, 53 | "user": { 54 | "login": "Login", 55 | "logout": "Logout", 56 | "register": "Registrieren", 57 | "form": { 58 | "username": "Nutzername", 59 | "email": "E-Mail", 60 | "password": "Passwort", 61 | "passwordConfirm": "Passwort bestätigen", 62 | "bio": "Bio", 63 | "image": "Bild", 64 | "backToLogin": "Zurück zum Login", 65 | "backToHome": "Zurück zu Home", 66 | "needAccount": "Noch kein Account?", 67 | "error": { 68 | "missing": "Bitte alle geforderten Felder füllen!", 69 | "passwordMismatch": "Bitte die gleichen Passwörter eingeben!", 70 | "validEmail": "Bitte gib eine korrekte E-Mail Adresse ein!", 71 | "validUsername": "Bitte einen Nutzernamen angeben!" 72 | }, 73 | "saved": "Profil aktualisiert!" 74 | }, 75 | "follow": "Folgen", 76 | "unfollow": "Entfolgen", 77 | "feed": { 78 | "user": "Meine Beiträge", 79 | "favorited": "Favorisierte Beiträge" 80 | } 81 | }, 82 | "comment": { 83 | "empty": "Keine Kommentare!", 84 | "response": "Schreib einen Kommentar...", 85 | "inResponse": "Reaktion auf", 86 | "publish": "Veröffentlichen", 87 | "delete": { 88 | "confirm": "Möchtest du diesen Kommentar wirklich löschen?" 89 | } 90 | }, 91 | "error": { 92 | "general": "Ein Fehler ist aufgetreten!", 93 | "login": "Bitte einloggen!" 94 | }, 95 | "validation": { 96 | "notEmpty": "Das darf nicht leer sein!" 97 | } 98 | } -------------------------------------------------------------------------------- /app/i18n/en.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.name": "Conduit", 3 | "title": "Conduit", 4 | "description": "A NativeScript RealWorld Example App", 5 | "general": { 6 | "save": "Save", 7 | "close": "Close" 8 | }, 9 | "about": { 10 | "realworld": "This app is an exemplary project to show how a Medium.com clone (called Conduit) is built using NativeScript to connect to any other backend from ", 11 | "thinkster": "RealWorld example projects have been initiated by Thinkster.io to provide a real-world scenario as tutorial to see how any different backend/frontend programming language and/or framework could work out in your project.", 12 | "insanitydesign": "This example stack has been created by INsanityDesign.com for RealWorld.io and is licensed under MIT. You can checkout the full source at GitHub.com." 13 | }, 14 | "settings": { 15 | "title": "Settings", 16 | "url": "Backend URL", 17 | "urlHint": "URL implementing the Conduit backend spec", 18 | "urlWarning": "Please enter a valid URL", 19 | "reset": "Reset" 20 | }, 21 | "drawer": { 22 | "login": "Login/Register", 23 | "logout": "Logout", 24 | "home": "Home", 25 | "settings": "Settings", 26 | "about": "About", 27 | "createdBy": "Created by https://github.com/nea/", 28 | "feedback": { 29 | "loggedOut": "You have been logged out successfully!" 30 | } 31 | }, 32 | "article": { 33 | "feed": { 34 | "user": "Your Feed", 35 | "global": "Global Feed" 36 | }, 37 | "loading": "Loading articles...", 38 | "loaded": "Loaded articles", 39 | "add": "Add article", 40 | "edit": "Edit article", 41 | "delete": "Delete article", 42 | "empty": "No articles found!", 43 | "form": { 44 | "title": "Title", 45 | "description": "Description", 46 | "body": "Body", 47 | "tags": "Tags", 48 | "confirmDelete": "Are you sure you want to delete this article?", 49 | "saved": "Article saved!", 50 | "deleted": "Article deleted!" 51 | } 52 | }, 53 | "user": { 54 | "login": "Login", 55 | "logout": "Logout", 56 | "register": "Register", 57 | "form": { 58 | "username": "Username", 59 | "email": "Email", 60 | "password": "Password", 61 | "passwordConfirm": "Confirm password", 62 | "bio": "Bio", 63 | "image": "Image", 64 | "backToLogin": "Back to login", 65 | "backToHome": "Back to home", 66 | "needAccount": "Don't have an account?", 67 | "error": { 68 | "missing": "Please provide all requested inputs!", 69 | "passwordMismatch": "Please provide matching passwords!", 70 | "validEmail": "Please enter a valid Email!", 71 | "validUsername": "Please provide a username!" 72 | }, 73 | "saved": "Profile updated!" 74 | }, 75 | "follow": "Follow", 76 | "unfollow": "Unfollow", 77 | "feed": { 78 | "user": "My Articles", 79 | "favorited": "Favorited Articles" 80 | } 81 | }, 82 | "comment": { 83 | "empty": "No comments!", 84 | "response": "Write a response...", 85 | "inResponse": "In response to", 86 | "publish": "Publish", 87 | "delete": { 88 | "confirm": "Are you sure you want to delete this comment?" 89 | } 90 | }, 91 | "error": { 92 | "general": "An error occured!", 93 | "login": "Please login!" 94 | }, 95 | "validation": { 96 | "notEmpty": "This may not be empty!" 97 | } 98 | } -------------------------------------------------------------------------------- /app/main.aot.ts: -------------------------------------------------------------------------------- 1 | // this import should be first in order to load some required settings (like globals and reflect-metadata) 2 | import { platformNativeScript } from "nativescript-angular/platform-static"; 3 | 4 | import { AppModuleNgFactory } from "./app.module.ngfactory"; 5 | 6 | platformNativeScript().bootstrapModuleFactory(AppModuleNgFactory); 7 | -------------------------------------------------------------------------------- /app/main.ts: -------------------------------------------------------------------------------- 1 | // this import should be first in order to load some required settings (like globals and reflect-metadata) 2 | import { platformNativeScriptDynamic } from "nativescript-angular/platform"; 3 | 4 | import { AppModule } from "./app.module"; 5 | 6 | platformNativeScriptDynamic().bootstrapModule(AppModule); 7 | -------------------------------------------------------------------------------- /app/model/Article.ts: -------------------------------------------------------------------------------- 1 | import { Profile } from "~/model/Profile"; 2 | 3 | /** 4 | * 5 | */ 6 | export class Article { 7 | /** */ 8 | public slug?: string = null; 9 | /** */ 10 | public title: string = null; 11 | /** */ 12 | public description: string = null; 13 | /** */ 14 | public body: string = null; 15 | /** */ 16 | public tagList?: string[] = []; 17 | /** */ 18 | public createdAt?: Date = null; 19 | /** */ 20 | public updatedAt?: Date = null; 21 | /** */ 22 | public favorited?: boolean = false; 23 | /** */ 24 | public favoritesCount?: number = 0; 25 | /** */ 26 | public author?: Profile = null; 27 | } 28 | -------------------------------------------------------------------------------- /app/model/Articles.ts: -------------------------------------------------------------------------------- 1 | import { Article } from "~/model/Article"; 2 | 3 | /** 4 | * 5 | */ 6 | export class Articles { 7 | /** */ 8 | public articles: Article[] = []; 9 | /** */ 10 | public articlesCount: number = 0; 11 | } 12 | -------------------------------------------------------------------------------- /app/model/Comment.ts: -------------------------------------------------------------------------------- 1 | import { Profile } from "~/model/Profile"; 2 | 3 | /** 4 | * 5 | */ 6 | export class Comment { 7 | /** */ 8 | public id: number = 0; 9 | /** */ 10 | public body: string = null; 11 | /** */ 12 | public createdAt: Date = null; 13 | /** */ 14 | public updatedAt: Date = null; 15 | /** */ 16 | public author: Profile = null; 17 | } 18 | -------------------------------------------------------------------------------- /app/model/Profile.ts: -------------------------------------------------------------------------------- 1 | import * as validator from "validator"; 2 | 3 | /** 4 | * 5 | */ 6 | export class Profile { 7 | /** */ 8 | public username: string = null; 9 | /** */ 10 | public bio: string = null; 11 | /** */ 12 | public _image: string = null; 13 | /** */ 14 | public following: boolean = false; 15 | 16 | /** 17 | * @return The image url or a default 18 | */ 19 | public get image(): string { 20 | return validator.isURL(this._image) ? this._image : "https://static.productionready.io/images/smiley-cyrus.jpg"; 21 | } 22 | 23 | /** 24 | * @param image 25 | */ 26 | public set image(image: string) { 27 | this._image = image; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/model/User.ts: -------------------------------------------------------------------------------- 1 | import * as validator from "validator"; 2 | 3 | /** 4 | * 5 | */ 6 | export class User { 7 | /** */ 8 | public id?: number = null; 9 | /** */ 10 | public email: string = null; 11 | /** */ 12 | public token?: string = null; 13 | /** */ 14 | public username: string = null; 15 | /** */ 16 | public bio?: string = null; 17 | /** */ 18 | public _image?: string = null; 19 | /** */ 20 | public password?: string = null; 21 | /** */ 22 | public createdAt?: Date = null; 23 | /** */ 24 | public updatedAt?: Date = null; 25 | 26 | /** 27 | * 28 | */ 29 | public hasValidEmail(): boolean { 30 | return validator.isEmail(this.email); 31 | } 32 | 33 | /** 34 | * @return The image url or a default 35 | */ 36 | public get image(): string { 37 | return validator.isURL(this._image) ? this._image : "https://static.productionready.io/images/smiley-cyrus.jpg"; 38 | } 39 | 40 | /** 41 | * @param image 42 | */ 43 | public set image(image: string) { 44 | this._image = image; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/module/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; 2 | import { NativeScriptModule } from "nativescript-angular/nativescript.module"; 3 | import { NativeScriptUIDataFormModule } from "nativescript-ui-dataform/angular"; 4 | import { TNSFontIconModule } from "nativescript-ngx-fonticon"; 5 | import { NativeScriptLocalizeModule } from "nativescript-localize/angular"; 6 | import { NativeScriptUIAutoCompleteTextViewModule } from "nativescript-ui-autocomplete/angular"; 7 | import { NativeScriptUIListViewModule } from "nativescript-ui-listview/angular"; 8 | 9 | import { ArticleRouting } from "./article.routing"; 10 | 11 | import { CommentModule } from "~/module/comment/comment.module"; 12 | import { ServiceModule } from "~/service/service.module"; 13 | 14 | import { EditArticleComponent } from "./edit-article.component"; 15 | import { ListArticlesComponent } from "~/module/article/list-articles.component"; 16 | import { ViewArticleComponent } from "~/module/article/view-article.component"; 17 | 18 | @NgModule({ 19 | imports: [ 20 | NativeScriptModule, 21 | NativeScriptUIDataFormModule, 22 | NativeScriptLocalizeModule, 23 | NativeScriptUIAutoCompleteTextViewModule, 24 | NativeScriptUIListViewModule, 25 | ServiceModule, 26 | CommentModule, 27 | ArticleRouting, 28 | TNSFontIconModule 29 | ], 30 | declarations: [ListArticlesComponent, EditArticleComponent, ViewArticleComponent], 31 | exports: [ListArticlesComponent], 32 | schemas: [NO_ERRORS_SCHEMA] 33 | }) 34 | export class ArticleModule {} 35 | -------------------------------------------------------------------------------- /app/module/article/article.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { EditArticleComponent } from "./edit-article.component"; 5 | import { ViewArticleComponent } from "~/module/article/view-article.component"; 6 | 7 | // prettier-ignore 8 | const ArticleRoutes: Routes = [ 9 | { path: "editor", component: EditArticleComponent }, 10 | { path: 'editor/:slug', component: EditArticleComponent }, 11 | { path: 'article/:slug', component: ViewArticleComponent } 12 | ]; 13 | 14 | export const ArticleRouting: ModuleWithProviders = RouterModule.forChild(ArticleRoutes); 15 | -------------------------------------------------------------------------------- /app/module/article/article.scss: -------------------------------------------------------------------------------- 1 | // Import app variables 2 | @import '~/app-variables'; 3 | 4 | ActionBar { 5 | background-color: white; 6 | color: black; 7 | } 8 | 9 | .article { 10 | background-color: $brand-light; 11 | width: 100%; 12 | height: 100%; 13 | 14 | .add-comment { 15 | width: 100%; 16 | padding: 8 15 4 15; 17 | background-color: white; 18 | border-bottom-color: $brand-dark; 19 | border-bottom-width: 1; 20 | } 21 | } 22 | 23 | .article-item { 24 | padding: 0 0 8 0; 25 | background-color: $brand-light; 26 | } 27 | 28 | .article-item-content { 29 | padding: 8 15 4 15; 30 | background-color: white; 31 | border-bottom-color: $brand-dark; 32 | border-bottom-width: 1; 33 | } 34 | 35 | .favorited { 36 | color: green; 37 | } -------------------------------------------------------------------------------- /app/module/article/edit-article.component.html: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/module/article/edit-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; 2 | import { Router, ActivatedRoute } from "@angular/router"; 3 | import { RadDataFormComponent } from "nativescript-ui-dataform/angular/dataform-directives"; 4 | import { EntityProperty, DataFormEventData } from "nativescript-ui-dataform"; 5 | import { Article } from "~/model/Article"; 6 | import { ConduitService } from "~/service/ConduitService"; 7 | import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"; 8 | import { localize } from "nativescript-localize"; 9 | import { RadAutoCompleteTextViewComponent } from "nativescript-ui-autocomplete/angular/autocomplete-directives"; 10 | import { ObservableArray } from "tns-core-modules/data/observable-array/observable-array"; 11 | import { TokenModel } from "nativescript-ui-autocomplete"; 12 | import { PageRoute } from "nativescript-angular/router"; 13 | import { switchMap } from "rxjs/operators"; 14 | import { topmost } from "ui/frame"; 15 | import * as dialogs from "ui/dialogs"; 16 | 17 | @Component({ 18 | selector: "conduit-edit-article", 19 | moduleId: module.id, 20 | templateUrl: "./edit-article.component.html", 21 | styleUrls: ["./article.css"] 22 | }) 23 | export class EditArticleComponent implements OnInit { 24 | /** */ 25 | public title: string; 26 | /** */ 27 | public article: Article; 28 | /** */ 29 | public isLoading: boolean = false; 30 | /** */ 31 | private feedback: Feedback; 32 | /** */ 33 | @ViewChild("formArticle") protected formArticle: RadDataFormComponent; 34 | /** */ 35 | @ViewChild("tagsField") protected tagsField: RadAutoCompleteTextViewComponent; 36 | 37 | private tags: Array = new Array(); 38 | 39 | /** 40 | * 41 | * @param router 42 | * @param route 43 | */ 44 | constructor(private router: Router, private pageRoute: PageRoute, private conduit: ConduitService) { 45 | this.feedback = new Feedback(); 46 | 47 | //Get the given article or create a new 48 | this.title = localize("article.add"); 49 | this.article = new Article(); 50 | console.log(this.article); 51 | this.pageRoute.activatedRoute.pipe(switchMap(activatedRoute => activatedRoute.params)).forEach(params => { 52 | if (params["slug"]) { 53 | this.isLoading = true; 54 | this.title = localize("article.edit"); 55 | this.conduit 56 | .getArticle(params["slug"]) 57 | .subscribe( 58 | (article: Article) => { 59 | this.article = article; 60 | article.tagList.forEach(tag => { 61 | let token = new TokenModel(tag, undefined); 62 | this.tags.push(token); 63 | this.tagsField.autoCompleteTextView.addToken(token); 64 | }); 65 | }, 66 | error => { 67 | this.feedback.error({ 68 | title: localize("error.general"), 69 | message: JSON.stringify(error.error) 70 | }); 71 | } 72 | ) 73 | .add(() => { 74 | this.isLoading = false; 75 | }); 76 | } 77 | }); 78 | } 79 | 80 | /** 81 | * 82 | */ 83 | public ngOnInit() { 84 | this.conduit.getTags().subscribe((tags: string[]) => { 85 | tags.forEach(tag => { 86 | this.tags.push(new TokenModel(tag, null)); 87 | }); 88 | }); 89 | 90 | this.tagsField.autoCompleteTextView.loadSuggestionsAsync = text => { 91 | let items: Array = Object.assign([], this.tags); 92 | let promise = new Promise((resolve, reject) => { 93 | if (text !== "") { 94 | items.push(new TokenModel(text, null)); 95 | resolve(items); 96 | } else { 97 | reject(); 98 | } 99 | }); 100 | return promise; 101 | }; 102 | } 103 | 104 | /** 105 | * 106 | */ 107 | public onBack() { 108 | topmost().goBack(); 109 | } 110 | 111 | /** 112 | * 113 | * @param args 114 | */ 115 | public onTokenAdded(args) { 116 | this.article.tagList.push(args.token.text); 117 | } 118 | 119 | /** 120 | * 121 | * @param args 122 | */ 123 | public onTokenRemoved(args) { 124 | delete this.article.tagList[args.token.text]; 125 | } 126 | 127 | /** 128 | * 129 | */ 130 | public onSave() { 131 | this.formArticle.dataForm.validateAll().then(result => { 132 | if (!this.formArticle.dataForm.hasValidationErrors()) { 133 | this.isLoading = true; 134 | this.conduit 135 | .addArticle(this.article.title, this.article.description, this.article.body, ...this.article.tagList) 136 | .subscribe( 137 | (article: Article) => { 138 | this.feedback.success({ 139 | title: localize("article.form.saved"), 140 | message: this.article.title 141 | }); 142 | }, 143 | error => { 144 | this.feedback.error({ 145 | title: localize("error.general"), 146 | message: JSON.stringify(error.error) 147 | }); 148 | } 149 | ) 150 | .add(() => { 151 | this.isLoading = false; 152 | this.router.navigate(["/home"]); 153 | }); 154 | } 155 | }); 156 | } 157 | 158 | /** 159 | * 160 | */ 161 | public onDelete() { 162 | dialogs.confirm(localize("article.form.confirmDelete")).then(result => { 163 | if (result) { 164 | this.isLoading = true; 165 | this.conduit 166 | .removeArticle(this.article.slug) 167 | .subscribe(() => { 168 | this.router.navigate(["/home"]); 169 | }) 170 | .add(() => { 171 | this.isLoading = false; 172 | }); 173 | } 174 | }); 175 | } 176 | 177 | /** 178 | * 179 | */ 180 | get tagsProvider(): Array { 181 | return this.tags; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/module/article/list-articles.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/module/article/list-articles.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { PropertyChangeData } from "ui/page"; 4 | import { ConduitService } from "~/service/ConduitService"; 5 | import { Article } from "~/model/Article"; 6 | import { Articles } from "~/model/Articles"; 7 | import { RadListViewComponent } from "nativescript-ui-listview/angular"; 8 | import { ListViewEventData } from "nativescript-ui-listview"; 9 | import { ObservableArray } from "tns-core-modules/data/observable-array/observable-array"; 10 | import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"; 11 | import { localize } from "nativescript-localize"; 12 | import { Subscription } from "rxjs"; 13 | 14 | @Component({ 15 | selector: "conduit-list-articles", 16 | moduleId: module.id, 17 | templateUrl: "./list-articles.component.html", 18 | styleUrls: ["./article.css"] 19 | }) 20 | export class ListArticlesComponent implements OnInit { 21 | /** */ 22 | @Input("isUserFeed") public isUserFeed: boolean = false; 23 | /** */ 24 | @Input("offsetInterval") public offsetInterval: number = 20; 25 | /** */ 26 | @Input("tag") public tag: string; 27 | /** */ 28 | @Input("author") public author: string; 29 | /** */ 30 | @Input("favorited") public favorited: string; 31 | /** */ 32 | @Input("limit") public limit: number = 20; 33 | /** */ 34 | @Input("offset") public offset: number = 0; 35 | 36 | /** */ 37 | public articles: ObservableArray
= new ObservableArray
(); 38 | /** */ 39 | public isLoading: boolean = false; 40 | /** */ 41 | private feedback: Feedback; 42 | 43 | /** 44 | * 45 | * @param router 46 | * @param conduit 47 | */ 48 | constructor(private router: Router, private conduit: ConduitService) { 49 | this.feedback = new Feedback(); 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | public ngOnInit() { 56 | this.loadArticles(); 57 | } 58 | 59 | /** 60 | * 61 | */ 62 | protected loadArticles(): Subscription { 63 | this.isLoading = true; 64 | if (this.isUserFeed) { 65 | return this.conduit 66 | .getArticlesFeed(this.limit, this.offset) 67 | .subscribe(this.onLoadingArticles, this.onLoadingError) 68 | .add(() => { 69 | this.onLoadingComplete(); 70 | }); 71 | } else { 72 | return this.conduit 73 | .getArticles(this.tag, this.author, this.favorited, this.limit, this.offset) 74 | .subscribe(this.onLoadingArticles, this.onLoadingError) 75 | .add(() => { 76 | this.onLoadingComplete(); 77 | }); 78 | } 79 | } 80 | 81 | /** 82 | * 83 | * @param args 84 | */ 85 | public onPullToRefresh(args: ListViewEventData) { 86 | //Reset articles 87 | this.articles = new ObservableArray
(); 88 | //Reload 89 | this.loadArticles().add(() => { 90 | args.object.notifyPullToRefreshFinished(); 91 | }); 92 | } 93 | 94 | /** 95 | * 96 | * @param args 97 | */ 98 | public onLoadMoreData(args: ListViewEventData) { 99 | //Increase offset 100 | this.offset += this.offsetInterval; 101 | //and load more data 102 | this.loadArticles().add(() => { 103 | args.object.notifyLoadOnDemandFinished(); 104 | args.returnValue = true; 105 | }); 106 | } 107 | 108 | /** 109 | * 110 | */ 111 | protected onLoadingArticles = (articles: Articles) => { 112 | this.articles.push(articles.articles); 113 | }; 114 | 115 | /** 116 | * 117 | */ 118 | protected onLoadingError = error => { 119 | this.feedback.error({ 120 | title: localize("error.general"), 121 | message: error 122 | }); 123 | }; 124 | 125 | /** 126 | * 127 | */ 128 | protected onLoadingComplete = () => { 129 | this.isLoading = false; 130 | }; 131 | 132 | /** 133 | * 134 | * @param username 135 | */ 136 | public onAuthor(username) { 137 | this.router.navigate([`/profile/${username}`]); 138 | } 139 | 140 | /** 141 | * 142 | * @param articleSlug 143 | */ 144 | public onArticle(articleSlug: string) { 145 | this.router.navigate([`/article/${articleSlug}`]); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/module/article/view-article.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/module/article/view-article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ViewContainerRef, ElementRef } from "@angular/core"; 2 | import { Router, ActivatedRoute } from "@angular/router"; 3 | import { ConduitService } from "~/service/ConduitService"; 4 | import { PageRoute } from "nativescript-angular/router"; 5 | import { Article } from "~/model/Article"; 6 | import { switchMap } from "rxjs/operators"; 7 | import { Feedback } from "nativescript-feedback"; 8 | import { topmost } from "ui/frame"; 9 | import * as SocialShare from "nativescript-social-share"; 10 | import { UserService } from "~/service/UserService"; 11 | import { ModalDialogService } from "nativescript-angular/directives/dialogs"; 12 | import { WriteCommentModal } from "~/module/comment/write-comment-modal.component"; 13 | import { ListCommentsComponent } from "~/module/comment/list-comments.component"; 14 | import { markdown } from "markdown"; 15 | import { localize } from "nativescript-localize"; 16 | import { ActionBar } from "tns-core-modules/ui/action-bar/action-bar"; 17 | 18 | @Component({ 19 | selector: "conduit-view-article", 20 | moduleId: module.id, 21 | templateUrl: "./view-article.component.html", 22 | styleUrls: ["./article.css"], 23 | providers: [ModalDialogService] 24 | }) 25 | export class ViewArticleComponent implements OnInit { 26 | /** */ 27 | public article: Article; 28 | /** */ 29 | public articleBody: string = ""; 30 | /** */ 31 | public isLoading: boolean = false; 32 | /** */ 33 | protected feedback: Feedback; 34 | /** */ 35 | @ViewChild("commentsList") protected commentsList: ListCommentsComponent; 36 | 37 | /** 38 | * 39 | * @param router 40 | * @param pageRoute 41 | * @param conduit 42 | */ 43 | constructor( 44 | private router: Router, 45 | private pageRoute: PageRoute, 46 | protected conduit: ConduitService, 47 | public userService: UserService, 48 | protected modal: ModalDialogService, 49 | protected vcRef: ViewContainerRef 50 | ) { 51 | this.feedback = new Feedback(); 52 | 53 | // 54 | this.pageRoute.activatedRoute.pipe(switchMap(activatedRoute => activatedRoute.params)).forEach(params => { 55 | if (params["slug"]) { 56 | this.isLoading = true; 57 | this.conduit 58 | .getArticle(params["slug"]) 59 | .subscribe( 60 | (article: Article) => { 61 | this.article = article; 62 | this.articleBody = markdown.toHTML(article.body, "Maruku"); 63 | }, 64 | error => { 65 | this.feedback.error({ 66 | title: localize("error.general"), 67 | message: error 68 | }); 69 | this.onBack(); 70 | } 71 | ) 72 | .add(() => { 73 | this.isLoading = false; 74 | }); 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * 81 | */ 82 | public ngOnInit() {} 83 | 84 | /** 85 | * 86 | */ 87 | public onWriteComment() { 88 | this.modal 89 | .showModal(WriteCommentModal, { 90 | context: this.article, 91 | fullscreen: true, 92 | viewContainerRef: this.vcRef 93 | }) 94 | .then(res => { 95 | if (res) { 96 | this.commentsList.reloadComments(); 97 | } 98 | }); 99 | } 100 | 101 | /** 102 | * 103 | */ 104 | public onFavorited() { 105 | this.article.favorited = !this.article.favorited; 106 | this.conduit.favorArticle(this.article.slug, this.article.favorited).subscribe( 107 | (article: Article) => { 108 | this.article = article; 109 | }, 110 | error => { 111 | this.feedback.error({ 112 | title: localize("error.general"), 113 | message: error 114 | }); 115 | } 116 | ); 117 | } 118 | 119 | /** 120 | * 121 | * @param username 122 | */ 123 | public onAuthor(username) { 124 | this.router.navigate([`/profile/${username}`]); 125 | } 126 | 127 | /** 128 | * 129 | */ 130 | public onShare() { 131 | SocialShare.shareText(`${this.article.title} 132 | 133 | ${this.article.description} 134 | 135 | ${this.articleBody}`); 136 | } 137 | 138 | /** 139 | * 140 | */ 141 | public onEdit() { 142 | this.router.navigate([`/editor/${this.article.slug}`]); 143 | } 144 | 145 | /** 146 | * 147 | */ 148 | public onBack() { 149 | topmost().goBack(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/module/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; 2 | import { NativeScriptModule } from "nativescript-angular/nativescript.module"; 3 | import { TNSFontIconModule } from "nativescript-ngx-fonticon"; 4 | import { NativeScriptLocalizeModule } from "nativescript-localize/angular"; 5 | import { CommentRouting } from "./comment.routing"; 6 | import { ListCommentsComponent } from "~/module/comment/list-comments.component"; 7 | import { ServiceModule } from "~/service/service.module"; 8 | import { WriteCommentModal } from "~/module/comment/write-comment-modal.component"; 9 | import { NativeScriptUIListViewModule } from "nativescript-ui-listview/angular"; 10 | 11 | @NgModule({ 12 | imports: [ 13 | NativeScriptModule, 14 | NativeScriptLocalizeModule, 15 | NativeScriptUIListViewModule, 16 | ServiceModule, 17 | CommentRouting, 18 | TNSFontIconModule 19 | ], 20 | declarations: [ListCommentsComponent, WriteCommentModal], 21 | entryComponents: [ListCommentsComponent, WriteCommentModal], 22 | exports: [ListCommentsComponent, WriteCommentModal], 23 | schemas: [NO_ERRORS_SCHEMA] 24 | }) 25 | export class CommentModule {} 26 | -------------------------------------------------------------------------------- /app/module/comment/comment.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | import { ListCommentsComponent } from "~/module/comment/list-comments.component"; 4 | 5 | // prettier-ignore 6 | const CommentRoutes: Routes = [ 7 | { path: 'articles/:slug/comments', component: ListCommentsComponent } 8 | ]; 9 | 10 | export const CommentRouting: ModuleWithProviders = RouterModule.forChild(CommentRoutes); 11 | -------------------------------------------------------------------------------- /app/module/comment/comment.scss: -------------------------------------------------------------------------------- 1 | // Import app variables 2 | @import '~/app-variables'; 3 | 4 | .comment-item { 5 | padding: 0 0 8 0; 6 | background-color: $brand-light; 7 | } 8 | 9 | .comment-item-content { 10 | padding: 8 15 4 15; 11 | background-color: white; 12 | border-bottom-color: $brand-dark; 13 | border-bottom-width: 1; 14 | } 15 | 16 | .comment-reply-box { 17 | border-width: 1; 18 | border-color: $brand-primary; 19 | } 20 | -------------------------------------------------------------------------------- /app/module/comment/list-comments.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/module/comment/list-comments.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnInit, ViewChild, Input } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { Page, PropertyChangeData } from "ui/page"; 4 | import { ConduitService } from "~/service/ConduitService"; 5 | import { Article } from "~/model/Article"; 6 | import { Articles } from "~/model/Articles"; 7 | import { UserService } from "~/service/UserService"; 8 | import { User } from "~/model/User"; 9 | import { RadListViewComponent } from "nativescript-ui-listview/angular"; 10 | import { ListViewEventData } from "nativescript-ui-listview"; 11 | import { ObservableArray } from "tns-core-modules/data/observable-array/observable-array"; 12 | import { SegmentedBar, SegmentedBarItem } from "ui/segmented-bar"; 13 | import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"; 14 | import { localize } from "nativescript-localize"; 15 | import { Subscription } from "rxjs"; 16 | import * as dialogs from "ui/dialogs"; 17 | import { ListView } from "ui/list-view"; 18 | 19 | @Component({ 20 | selector: "conduit-list-comments", 21 | moduleId: module.id, 22 | templateUrl: "./list-comments.component.html", 23 | styleUrls: ["./comment.css"] 24 | }) 25 | export class ListCommentsComponent implements OnInit { 26 | /** */ 27 | @Input("slug") public slug: string; 28 | /** */ 29 | public comments: ObservableArray = new ObservableArray(); 30 | /** */ 31 | public isLoading: boolean = false; 32 | /** */ 33 | private feedback: Feedback; 34 | 35 | /** 36 | * 37 | * @param router 38 | * @param conduit 39 | */ 40 | constructor(private router: Router, private conduit: ConduitService, private userService: UserService) { 41 | this.feedback = new Feedback(); 42 | } 43 | 44 | /** 45 | * 46 | */ 47 | public ngOnInit() { 48 | this.reloadComments(); 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | public reloadComments(): Subscription { 55 | this.isLoading = true; 56 | return this.conduit 57 | .getComments(this.slug) 58 | .subscribe(this.onLoadingComments, this.onLoadingError) 59 | .add(() => { 60 | this.onLoadingComplete(); 61 | }); 62 | } 63 | 64 | /** 65 | * 66 | */ 67 | protected onLoadingComments = (comments: Comment[]) => { 68 | this.comments = new ObservableArray(); 69 | this.comments.push(comments); 70 | }; 71 | 72 | /** 73 | * 74 | */ 75 | protected onLoadingError = error => { 76 | this.feedback.error({ 77 | title: localize("error.general"), 78 | message: error 79 | }); 80 | }; 81 | 82 | /** 83 | * 84 | */ 85 | protected onLoadingComplete = () => { 86 | this.isLoading = false; 87 | }; 88 | 89 | /** 90 | * 91 | * @param args 92 | */ 93 | public onAuthor(args) { 94 | this.router.navigate([`/profile/${args.object.text}`]); 95 | } 96 | 97 | /** 98 | * @param commentId 99 | */ 100 | public onDelete(commentId: number) { 101 | dialogs.confirm(localize("comment.delete.confirm")).then(result => { 102 | if (result) { 103 | this.isLoading = true; 104 | this.conduit 105 | .deleteComment(this.slug, commentId) 106 | .subscribe(() => {}, this.onLoadingError) 107 | .add(() => { 108 | this.reloadComments(); 109 | }); 110 | } 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/module/comment/write-comment-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/module/comment/write-comment-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ElementRef } from "@angular/core"; 2 | import { ModalDialogParams } from "nativescript-angular/directives/dialogs"; 3 | import { Page } from "tns-core-modules/ui/page/page"; 4 | import { Article } from "~/model/Article"; 5 | import { TextField } from "ui/text-field"; 6 | import { ConduitService } from "~/service/ConduitService"; 7 | 8 | @Component({ 9 | selector: "conduit-write-comment-modal", 10 | moduleId: module.id, 11 | templateUrl: "./write-comment-modal.component.html", 12 | styleUrls: ["./comment.css"] 13 | }) 14 | export class WriteCommentModal { 15 | /** */ 16 | public article: Article; 17 | /** */ 18 | public isLoading: boolean = false; 19 | /** */ 20 | @ViewChild("txtComment") protected txtComment: ElementRef; 21 | 22 | /** 23 | * 24 | * @param params 25 | * @param page 26 | * @param conduit 27 | */ 28 | public constructor(private params: ModalDialogParams, private page: Page, protected conduit: ConduitService) { 29 | this.article = params.context; 30 | this.page.on("unloaded", () => { 31 | this.params.closeCallback(); 32 | }); 33 | } 34 | 35 | /** 36 | * 37 | */ 38 | public onClose() { 39 | this.params.closeCallback(); 40 | } 41 | 42 | /** 43 | * 44 | */ 45 | public onSubmit() { 46 | this.isLoading = true; 47 | let commentField = this.txtComment.nativeElement; 48 | this.conduit.addComment(this.article.slug, commentField.text).subscribe(() => { 49 | this.params.closeCallback(commentField.text); 50 | }).add(() => { 51 | this.isLoading = false; 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/module/home/about-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/module/home/about-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ElementRef } from "@angular/core"; 2 | import { ModalDialogParams } from "nativescript-angular/directives/dialogs"; 3 | import * as utils from "utils/utils"; 4 | 5 | @Component({ 6 | selector: "conduit-about-modal", 7 | moduleId: module.id, 8 | templateUrl: "./about-modal.component.html", 9 | styleUrls: ["./home.css"] 10 | }) 11 | export class AboutModal { 12 | /** 13 | * 14 | * @param params 15 | */ 16 | public constructor(private params: ModalDialogParams) {} 17 | 18 | /** 19 | * 20 | */ 21 | public onClose() { 22 | this.params.closeCallback(); 23 | } 24 | 25 | /** 26 | * 27 | * @param url 28 | */ 29 | public onLink(url: string) { 30 | utils.openUrl(url); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/module/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/module/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { UserService } from "~/service/UserService"; 4 | import { SegmentedBar, SegmentedBarItem } from "ui/segmented-bar"; 5 | import { localize } from "nativescript-localize"; 6 | import { ListArticlesComponent } from "~/module/article/list-articles.component"; 7 | import { RadSideDrawer } from "nativescript-ui-sidedrawer"; 8 | import * as app from "application"; 9 | import { PropertyChangeData } from "tns-core-modules/ui/page/page"; 10 | 11 | import { registerElement } from "nativescript-angular/element-registry"; 12 | registerElement("Fab", () => require("nativescript-floatingactionbutton").Fab); 13 | 14 | @Component({ 15 | selector: "conduit-home", 16 | moduleId: module.id, 17 | templateUrl: "./home.component.html", 18 | styleUrls: ["./home.css"] 19 | }) 20 | export class HomeComponent implements OnInit { 21 | /** */ 22 | public isLoading: boolean = false; 23 | /** */ 24 | protected isUserFeed: boolean = false; 25 | 26 | /** 27 | * 28 | * @param router 29 | * @param userService 30 | */ 31 | constructor(protected router: Router, public userService: UserService) {} 32 | 33 | /** 34 | * 35 | */ 36 | public ngOnInit() {} 37 | 38 | /** 39 | * 40 | */ 41 | public get IsUserFeed() { 42 | return this.userService.getUser() !== null ? this.isUserFeed : false; 43 | } 44 | 45 | /** 46 | * 47 | * @param args 48 | */ 49 | public onFeedChange(args: PropertyChangeData) { 50 | this.isUserFeed = args.value === 0; 51 | } 52 | 53 | /** 54 | * 55 | */ 56 | public onAddArticle() { 57 | this.router.navigate(["/editor"]); 58 | } 59 | 60 | /** 61 | * 62 | */ 63 | public onDrawer() { 64 | const sideDrawer = app.getRootView(); 65 | sideDrawer.showDrawer(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/module/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; 2 | import { NativeScriptModule } from "nativescript-angular/nativescript.module"; 3 | import { TNSFontIconModule } from "nativescript-ngx-fonticon"; 4 | import { NativeScriptLocalizeModule } from "nativescript-localize/angular"; 5 | 6 | import { HomeRouting } from "./home.routing"; 7 | import { HomeComponent } from "./home.component"; 8 | 9 | import { ArticleModule } from "~/module/article/article.module"; 10 | import { UserModule } from "~/module/user/user.module"; 11 | import { ServiceModule } from "~/service/service.module"; 12 | 13 | import { AboutModal } from "~/module/home/about-modal.component"; 14 | import { SettingsComponent } from "~/module/home/settings.component"; 15 | 16 | @NgModule({ 17 | imports: [NativeScriptModule, HomeRouting, NativeScriptLocalizeModule, ServiceModule, ArticleModule, UserModule, TNSFontIconModule], 18 | entryComponents: [AboutModal], 19 | declarations: [HomeComponent, AboutModal, SettingsComponent], 20 | schemas: [NO_ERRORS_SCHEMA] 21 | }) 22 | export class HomeModule {} 23 | -------------------------------------------------------------------------------- /app/module/home/home.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { HomeComponent } from "./home.component"; 5 | import { SettingsComponent } from "./settings.component"; 6 | 7 | // prettier-ignore 8 | const HomeRoutes: Routes = [ 9 | { path: "home", component: HomeComponent }, 10 | { path: "settings", component: SettingsComponent } 11 | ]; 12 | 13 | export const HomeRouting: ModuleWithProviders = RouterModule.forChild(HomeRoutes); 14 | -------------------------------------------------------------------------------- /app/module/home/home.scss: -------------------------------------------------------------------------------- 1 | // Import app variables 2 | @import '~/app-variables'; 3 | 4 | .article-item { 5 | padding: 0 0 8 0; 6 | background-color: $brand-light; 7 | } 8 | 9 | .article-item-content { 10 | padding: 8 15 4 15; 11 | background-color: white; 12 | border-bottom-color: $brand-dark; 13 | border-bottom-width: 1; 14 | } 15 | 16 | .fab { 17 | height: 48; 18 | width: 48; 19 | margin: 10; 20 | background-color: $brand-primary; 21 | horizontal-align: right; 22 | vertical-align: bottom; 23 | } -------------------------------------------------------------------------------- /app/module/home/settings.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/module/home/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, ElementRef } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { AbstractHttpService } from "~/service/AbstractHttpService"; 4 | import { setString, getString } from "tns-core-modules/application-settings/application-settings"; 5 | import { Feedback } from "nativescript-feedback"; 6 | import { localize } from "nativescript-localize"; 7 | import * as isURL from "validator/lib/isURL"; 8 | import { topmost } from "tns-core-modules/ui/frame/frame"; 9 | import { UserService } from "~/service/UserService"; 10 | 11 | @Component({ 12 | selector: "conduit-settings", 13 | moduleId: module.id, 14 | templateUrl: "./settings.component.html", 15 | styleUrls: ["./home.css"] 16 | }) 17 | export class SettingsComponent implements OnInit { 18 | /** */ 19 | private feedback: Feedback; 20 | /** */ 21 | public url: string; 22 | /** */ 23 | @ViewChild("txtUrl") protected txtUrl: ElementRef; 24 | 25 | /** 26 | * 27 | * @param router 28 | * @param userService 29 | */ 30 | constructor(protected router: Router, public userService: UserService) { 31 | this.feedback = new Feedback(); 32 | } 33 | 34 | /** 35 | * 36 | */ 37 | public ngOnInit() { 38 | this.url = getString("apiUrl", AbstractHttpService.PRODUCTIONREADY_IO_API_BASE_URL); 39 | } 40 | 41 | /** 42 | * 43 | */ 44 | public onSave() { 45 | if (!this.txtUrl.nativeElement.text || !isURL(this.txtUrl.nativeElement.text)) { 46 | this.feedback.warning({ 47 | title: localize("error.general"), 48 | message: localize("settings.urlWarning") 49 | }); 50 | return; 51 | } 52 | this.save(this.txtUrl.nativeElement.text); 53 | topmost().goBack(); 54 | } 55 | 56 | /** 57 | * Reset the backend url back to the productionready.io url 58 | */ 59 | public onReset() { 60 | this.save(AbstractHttpService.PRODUCTIONREADY_IO_API_BASE_URL); 61 | } 62 | 63 | /** 64 | * 65 | */ 66 | public onBack() { 67 | topmost().goBack(); 68 | } 69 | 70 | /** 71 | * 72 | * @param url 73 | */ 74 | protected save(url) { 75 | setString("apiUrl", url); 76 | this.url = url; 77 | this.txtUrl.nativeElement.text = url; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/module/user/edit-profile.component.html: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /app/module/user/edit-profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from "@angular/core"; 2 | import { Router, ActivatedRoute } from "@angular/router"; 3 | import { PropertyChangeData } from "ui/page"; 4 | import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"; 5 | import { localize } from "nativescript-localize"; 6 | import { Profile } from "~/model/Profile"; 7 | import { UserService } from "~/service/UserService"; 8 | import { PageRoute } from "nativescript-angular/router"; 9 | import { switchMap } from "rxjs/operators"; 10 | import { topmost } from "ui/frame"; 11 | import { RadDataFormComponent } from "nativescript-ui-dataform/angular/dataform-directives"; 12 | import { User } from "~/model/User"; 13 | 14 | @Component({ 15 | selector: "conduit-edit-profile", 16 | moduleId: module.id, 17 | templateUrl: "./edit-profile.component.html", 18 | styleUrls: ["./user.css"] 19 | }) 20 | export class EditProfileComponent implements OnInit { 21 | /** */ 22 | public isLoading: boolean = false; 23 | /** */ 24 | private feedback: Feedback; 25 | /** */ 26 | public user: User = new User(); 27 | /** */ 28 | @ViewChild("formProfile") protected formProfile: RadDataFormComponent; 29 | 30 | /** 31 | * 32 | * @param router 33 | * @param userService 34 | */ 35 | constructor(private router: Router, public userService: UserService) { 36 | this.feedback = new Feedback(); 37 | Object.assign(this.user, this.userService.getUser()); 38 | } 39 | 40 | /** 41 | * 42 | */ 43 | public ngOnInit() {} 44 | 45 | /** 46 | * 47 | */ 48 | public onSave() { 49 | this.formProfile.dataForm.validateAll().then(result => { 50 | if (!this.formProfile.dataForm.hasValidationErrors() && this.user.hasValidEmail()) { 51 | this.isLoading = true; 52 | //Update password (if any is given) 53 | if (this.user.password) { 54 | this.userService.updatePassword(this.user.password).subscribe( 55 | (user: User) => {}, 56 | error => { 57 | this.feedback.error({ 58 | title: localize("error.general"), 59 | message: JSON.stringify(error.error) 60 | }); 61 | } 62 | ); 63 | } 64 | //Update User 65 | this.userService 66 | .updateUser(this.user) 67 | .subscribe( 68 | (user: User) => { 69 | this.feedback.success({ 70 | title: localize("user.form.saved"), 71 | message: this.user.username 72 | }); 73 | }, 74 | error => { 75 | this.feedback.error({ 76 | title: localize("error.general"), 77 | message: JSON.stringify(error.error) 78 | }); 79 | } 80 | ) 81 | .add(() => { 82 | this.isLoading = false; 83 | this.router.navigate(["/home"]); 84 | }); 85 | } 86 | }); 87 | } 88 | 89 | /** 90 | * 91 | */ 92 | public onBack() { 93 | topmost().goBack(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/module/user/login.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/module/user/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, ViewChild } from "@angular/core"; 2 | import { Router } from "@angular/router"; 3 | import { alert, prompt } from "tns-core-modules/ui/dialogs"; 4 | import { Page } from "tns-core-modules/ui/page"; 5 | import { User } from "~/model/User"; 6 | import { UserService } from "~/service/UserService"; 7 | import { Feedback } from "nativescript-feedback"; 8 | import { localize } from "nativescript-localize"; 9 | import { RouterExtensions } from "nativescript-angular/router"; 10 | import * as isEmail from "validator/lib/isEmail"; 11 | 12 | @Component({ 13 | selector: "conduit-login", 14 | moduleId: module.id, 15 | templateUrl: "./login.component.html", 16 | styleUrls: ["./user.css"] 17 | }) 18 | export class LoginComponent { 19 | /** */ 20 | public isLoggingIn = true; 21 | /** */ 22 | public isLoading = false; 23 | /** */ 24 | public user: User; 25 | /** */ 26 | protected feedback: Feedback; 27 | /** */ 28 | @ViewChild("username") public username: ElementRef; 29 | /** */ 30 | @ViewChild("password") public password: ElementRef; 31 | /** */ 32 | @ViewChild("confirmPassword") public confirmPassword: ElementRef; 33 | 34 | /** 35 | * 36 | * @param page 37 | * @param router 38 | * @param userService 39 | */ 40 | constructor(protected page: Page, protected routerExtensions: RouterExtensions, public userService: UserService) { 41 | this.feedback = new Feedback(); 42 | this.page.actionBarHidden = true; 43 | this.user = new User(); 44 | } 45 | 46 | /** 47 | * 48 | */ 49 | public onSubmit() { 50 | if (!this.user.email || !this.password.nativeElement.text) { 51 | this.showError("user.form.error.missing"); 52 | return; 53 | } 54 | if (!isEmail(this.user.email)) { 55 | this.showError("user.form.error.validEmail"); 56 | return; 57 | } 58 | 59 | if (this.isLoggingIn) { 60 | this.login(); 61 | } else { 62 | this.register(); 63 | } 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | protected login() { 70 | this.isLoading = true; 71 | this.userService 72 | .login(this.user.email, this.password.nativeElement.text) 73 | .subscribe( 74 | () => { 75 | this.onBack(); 76 | }, 77 | error => { 78 | this.showError(JSON.stringify(error.error)); 79 | } 80 | ) 81 | .add(() => { 82 | this.isLoading = false; 83 | }); 84 | } 85 | 86 | /** 87 | * 88 | */ 89 | protected register() { 90 | if (this.password.nativeElement.text !== this.confirmPassword.nativeElement.text) { 91 | this.showError("user.form.error.passwordMismatch"); 92 | return; 93 | } 94 | if (!this.user.username) { 95 | this.showError("user.form.error.validUsername"); 96 | return; 97 | } 98 | this.isLoading = true; 99 | this.userService 100 | .register(this.user.username, this.user.email, this.password.nativeElement.text) 101 | .subscribe(() => { 102 | this.onBack(); 103 | }) 104 | .add(() => { 105 | this.isLoading = false; 106 | }); 107 | } 108 | 109 | /** 110 | * 111 | */ 112 | public onBack() { 113 | this.routerExtensions.navigate(["/home"], { clearHistory: true }); 114 | } 115 | 116 | /** 117 | * @param messageKey Key to localize as error message 118 | */ 119 | protected showError(messageKey: string) { 120 | this.feedback.error({ 121 | title: localize("error.general"), 122 | message: localize(messageKey) 123 | }); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/module/user/profile.component.html: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /app/module/user/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { Router, ActivatedRoute } from "@angular/router"; 3 | import { PropertyChangeData } from "ui/page"; 4 | import { RadListViewComponent } from "nativescript-ui-listview/angular"; 5 | import { ListViewEventData } from "nativescript-ui-listview"; 6 | import { ObservableArray } from "tns-core-modules/data/observable-array/observable-array"; 7 | import { Feedback, FeedbackType, FeedbackPosition } from "nativescript-feedback"; 8 | import { localize } from "nativescript-localize"; 9 | import { Profile } from "~/model/Profile"; 10 | import { UserService } from "~/service/UserService"; 11 | import { PageRoute } from "nativescript-angular/router"; 12 | import { switchMap } from "rxjs/operators"; 13 | import { topmost } from "ui/frame"; 14 | 15 | @Component({ 16 | selector: "conduit-profile", 17 | moduleId: module.id, 18 | templateUrl: "./profile.component.html", 19 | styleUrls: ["./user.css"] 20 | }) 21 | export class ProfileComponent implements OnInit { 22 | /** */ 23 | public isLoading: boolean = false; 24 | /** */ 25 | public isMyArticles: boolean = true; 26 | /** */ 27 | private feedback: Feedback; 28 | /** */ 29 | public profile: Profile; 30 | 31 | /** 32 | * 33 | * @param router 34 | * @param conduit 35 | */ 36 | constructor(private router: Router, private pageRoute: PageRoute, public userService: UserService) { 37 | this.feedback = new Feedback(); 38 | 39 | //Get the given username's profile 40 | this.pageRoute.activatedRoute.pipe(switchMap(activatedRoute => activatedRoute.params)).forEach(params => { 41 | this.isLoading = true; 42 | this.userService 43 | .getProfile(params["username"]) 44 | .subscribe( 45 | (profile: Profile) => { 46 | this.profile = profile; 47 | }, 48 | error => { 49 | this.feedback.error({ 50 | title: localize("error.general"), 51 | message: error 52 | }); 53 | } 54 | ) 55 | .add(() => { 56 | this.isLoading = false; 57 | }); 58 | }); 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | public ngOnInit() {} 65 | 66 | /** 67 | * 68 | * @param args 69 | */ 70 | public onFeedChange(args: PropertyChangeData) { 71 | this.isMyArticles = args.value === 0; 72 | } 73 | 74 | /** 75 | * Just follow/unfollow and set the local profile value expecting success. 76 | */ 77 | public onFollow() { 78 | this.profile.following = !this.profile.following; 79 | this.userService.followUser(this.profile.username, this.profile.following).subscribe( 80 | (profile: Profile) => { 81 | this.profile = profile; 82 | }, 83 | error => { 84 | this.feedback.error({ 85 | title: localize("error.general"), 86 | message: error 87 | }); 88 | } 89 | ); 90 | } 91 | 92 | /** 93 | * 94 | */ 95 | public onBack() { 96 | topmost().goBack(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/module/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; 2 | import { NativeScriptModule } from "nativescript-angular/nativescript.module"; 3 | import { NativeScriptFormsModule } from "nativescript-angular/forms"; 4 | import { TNSFontIconModule } from "nativescript-ngx-fonticon"; 5 | import { NativeScriptLocalizeModule } from "nativescript-localize/angular"; 6 | import { NativeScriptUIDataFormModule } from "nativescript-ui-dataform/angular"; 7 | 8 | import { UserRouting } from "./user.routing"; 9 | 10 | import { ArticleModule } from "~/module/article/article.module"; 11 | import { ServiceModule } from "~/service/service.module"; 12 | 13 | import { ProfileComponent } from "./profile.component"; 14 | import { LoginComponent } from "./login.component"; 15 | import { EditProfileComponent } from "~/module/user/edit-profile.component"; 16 | 17 | @NgModule({ 18 | imports: [ 19 | NativeScriptModule, 20 | NativeScriptFormsModule, 21 | NativeScriptLocalizeModule, 22 | NativeScriptUIDataFormModule, 23 | ServiceModule, 24 | ArticleModule, 25 | UserRouting, 26 | TNSFontIconModule 27 | ], 28 | declarations: [ProfileComponent, LoginComponent, EditProfileComponent], 29 | schemas: [NO_ERRORS_SCHEMA] 30 | }) 31 | export class UserModule {} 32 | -------------------------------------------------------------------------------- /app/module/user/user.routing.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from "@angular/core"; 2 | import { Routes, RouterModule } from "@angular/router"; 3 | 4 | import { ProfileComponent } from "./profile.component"; 5 | import { LoginComponent } from "./login.component"; 6 | import { EditProfileComponent } from "~/module/user/edit-profile.component"; 7 | 8 | // prettier-ignore 9 | const UserRoutes: Routes = [ 10 | { path: 'profile/:username', component: ProfileComponent }, 11 | { path: 'user/settings', component: EditProfileComponent }, 12 | { path: 'login', component: LoginComponent } 13 | ]; 14 | 15 | export const UserRouting: ModuleWithProviders = RouterModule.forChild(UserRoutes); 16 | -------------------------------------------------------------------------------- /app/module/user/user.scss: -------------------------------------------------------------------------------- 1 | // Import app variables 2 | @import '~/app-variables'; 3 | 4 | ActionBar { 5 | background-color: white; 6 | color: black; 7 | border-width: 1; 8 | border-color: transparent; 9 | } 10 | 11 | .page { 12 | align-items: center; 13 | flex-direction: column; 14 | } 15 | 16 | .form { 17 | margin-left: 30; 18 | margin-right: 30; 19 | flex-grow: 2; 20 | vertical-align: middle; 21 | } 22 | 23 | .logo { 24 | margin-bottom: 12; 25 | height: 90; 26 | font-weight: bold; 27 | } 28 | 29 | .header { 30 | horizontal-align: center; 31 | font-size: 25; 32 | font-weight: 600; 33 | margin-bottom: 70; 34 | text-align: center; 35 | color: $brand-primary; 36 | } 37 | 38 | .input-field { 39 | margin-bottom: 25; 40 | } 41 | 42 | .input { 43 | font-size: 18; 44 | placeholder-color: $brand-dark; 45 | } 46 | 47 | .btn-primary { 48 | height: 50; 49 | margin: 30 5 15 5; 50 | background-color: $brand-primary; 51 | border-radius: 5; 52 | font-size: 20; 53 | font-weight: 600; 54 | } 55 | 56 | .login-label { 57 | horizontal-align: center; 58 | color: $brand-dark; 59 | font-size: 16; 60 | } 61 | 62 | .sign-up-label { 63 | margin-bottom: 20; 64 | } 65 | 66 | .bold { 67 | color: #000000; 68 | } 69 | 70 | .follow { 71 | color: green; 72 | } 73 | 74 | .unfollow { 75 | color: red; 76 | } -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "v8Flags": "--expose_gc" 4 | }, 5 | "main": "main.js", 6 | "name": "tns-template-blank-ng", 7 | "version": "4.0.0" 8 | } -------------------------------------------------------------------------------- /app/service/AbstractHttpService.ts: -------------------------------------------------------------------------------- 1 | import { Observable as RxObservable } from "rxjs"; 2 | import { HttpClient, HttpHeaders, HttpResponse, HttpErrorResponse } from "@angular/common/http"; 3 | import { getString, setString } from "application-settings"; 4 | import { UserService } from "~/service/UserService"; 5 | 6 | /** 7 | * 8 | */ 9 | export abstract class AbstractHttpService { 10 | /** 11 | * Default online URL in case no other is given 12 | */ 13 | public static PRODUCTIONREADY_IO_API_BASE_URL: string = "https://conduit.productionready.io/api"; 14 | 15 | /** 16 | * 17 | * @param http 18 | */ 19 | constructor(protected http: HttpClient) {} 20 | 21 | /** 22 | * 23 | * @param urlSuffix 24 | * @param params 25 | */ 26 | protected get(urlSuffix, params = {}): RxObservable { 27 | return this.http.get(AbstractHttpService.ApiUrl + urlSuffix, { headers: AbstractHttpService.Headers, params }); 28 | } 29 | 30 | /** 31 | * 32 | * @param urlSuffix 33 | * @param body 34 | */ 35 | protected post(urlSuffix, body = {}): RxObservable { 36 | return this.http.post(AbstractHttpService.ApiUrl + urlSuffix, body, { headers: AbstractHttpService.Headers }); 37 | } 38 | 39 | /** 40 | * 41 | * @param urlSuffix 42 | * @param body 43 | */ 44 | protected put(urlSuffix, body = {}): RxObservable { 45 | return this.http.put(AbstractHttpService.ApiUrl + urlSuffix, body, { headers: AbstractHttpService.Headers }); 46 | } 47 | 48 | /** 49 | * 50 | * @param urlSuffix 51 | */ 52 | protected delete(urlSuffix): RxObservable { 53 | return this.http.delete(AbstractHttpService.ApiUrl + urlSuffix, { headers: AbstractHttpService.Headers }); 54 | } 55 | 56 | /** 57 | * 58 | * @param error 59 | */ 60 | protected handleError(error: HttpErrorResponse) { 61 | return RxObservable.throw(error); 62 | } 63 | 64 | /** 65 | * 66 | */ 67 | public static get ApiUrl(): string { 68 | return getString("apiUrl", AbstractHttpService.PRODUCTIONREADY_IO_API_BASE_URL); 69 | } 70 | 71 | /** 72 | * 73 | */ 74 | public static set ApiUrl(apiUrl: string) { 75 | setString("apiUrl", apiUrl); 76 | } 77 | 78 | /** 79 | * 80 | */ 81 | public static get Headers(): HttpHeaders { 82 | let headers = { 83 | "X-Requested-With": "XMLHttpRequest", 84 | "Content-Type": "application/json; charset=utf-8", 85 | "Cache-Control": "no-cache", 86 | "Authorization": `Token ${getString("token", "")}` 87 | }; 88 | if (!!!getString("token")) { 89 | delete headers.Authorization; 90 | } 91 | return new HttpHeaders(headers); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/service/ConduitService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Observable as RxObservable } from "rxjs"; 3 | import { tap, map, catchError } from "rxjs/operators"; 4 | import { HttpClient, HttpHeaders, HttpResponse, HttpParams } from "@angular/common/http"; 5 | 6 | import { AbstractHttpService } from "~/service/AbstractHttpService"; 7 | import { UserService } from "~/service/UserService"; 8 | import { Article } from "~/model/Article"; 9 | import { localize } from "nativescript-localize"; 10 | 11 | /** 12 | * 13 | */ 14 | @Injectable() 15 | export class ConduitService extends AbstractHttpService { 16 | /** 17 | * 18 | * @param http 19 | */ 20 | constructor(protected http: HttpClient) { 21 | super(http); 22 | } 23 | 24 | /** 25 | * Filter by tag: `?tag=AngularJS` 26 | * Filter by author: `?author=jake` 27 | * Favorited by user: `?favorited=jake` 28 | * Limit number of articles (default is 20): `?limit=20` 29 | * Offset/skip number of articles (default is 0): `?offset=0` 30 | */ 31 | public getArticles( 32 | tag: string = "", 33 | author: string = "", 34 | favorited: string = "", 35 | limit: number = 20, 36 | offset: number = 0 37 | ): RxObservable { 38 | return this.get("/articles", { 39 | tag, 40 | author, 41 | favorited, 42 | limit: limit.toString(), 43 | offset: offset.toString() 44 | }); 45 | } 46 | 47 | /** 48 | * Limit number of articles (default is 20): `?limit=20` 49 | * Offset/skip number of articles (default is 0): `?offset=0` 50 | */ 51 | public getArticlesFeed(limit: number = 20, offset: number = 0): RxObservable { 52 | if (!UserService.IsLoggedIn()) { 53 | return RxObservable.throw(localize("error.login")); 54 | } 55 | return this.get("/articles/feed", { 56 | limit: limit.toString(), 57 | offset: offset.toString() 58 | }); 59 | } 60 | 61 | /** 62 | * 63 | * @param slug 64 | */ 65 | public getArticle(slug: string): RxObservable { 66 | return this.get(`/articles/${slug}`).pipe(map((data: any) => data.article)); 67 | } 68 | 69 | /** 70 | * 71 | * @param title 72 | * @param description 73 | * @param body 74 | * @param tags 75 | */ 76 | public addArticle(title: string, description: string, body: string, ...tagList: string[]): RxObservable { 77 | if (!UserService.IsLoggedIn()) { 78 | return RxObservable.throw(localize("error.login")); 79 | } 80 | let article: Article = { 81 | title, 82 | description, 83 | body, 84 | tagList 85 | }; 86 | return this.post("/articles", JSON.stringify(article)); 87 | } 88 | 89 | /** 90 | * 91 | * @param article 92 | */ 93 | public setArticle(article: Article): RxObservable { 94 | if (!UserService.IsLoggedIn()) { 95 | return RxObservable.throw(localize("error.login")); 96 | } 97 | return this.put(`/articles/${article.slug}`, JSON.stringify(article)); 98 | } 99 | 100 | /** 101 | * 102 | * @param slug 103 | */ 104 | public removeArticle(slug: string): RxObservable { 105 | if (!UserService.IsLoggedIn()) { 106 | return RxObservable.throw(localize("error.login")); 107 | } 108 | return this.delete(`/articles/${slug}`); 109 | } 110 | 111 | /** 112 | * 113 | * @param slug 114 | * @param favor 115 | */ 116 | public favorArticle(slug: string, favor: boolean = true): RxObservable { 117 | if (!UserService.IsLoggedIn()) { 118 | return RxObservable.throw(localize("error.login")); 119 | } 120 | if (favor) { 121 | return this.post(`/articles/${slug}/favorite`).pipe(map((data: any) => data.article)); 122 | } else { 123 | return this.delete(`/articles/${slug}/favorite`).pipe(map((data: any) => data.article)); 124 | } 125 | } 126 | 127 | /** 128 | * 129 | * @param slug 130 | * @param body 131 | */ 132 | public addComment(slug: string, body: string): RxObservable { 133 | if (!UserService.IsLoggedIn()) { 134 | return RxObservable.throw(localize("error.login")); 135 | } 136 | return this.post(`/articles/${slug}/comments`, { body }); 137 | } 138 | 139 | /** 140 | * 141 | * @param slug 142 | */ 143 | public getComments(slug: string): RxObservable { 144 | return this.get(`/articles/${slug}/comments`).pipe(map((data: any) => data.comments)); 145 | } 146 | 147 | /** 148 | * 149 | */ 150 | public deleteComment(slug: string, commentId: number): RxObservable { 151 | if (!UserService.IsLoggedIn()) { 152 | return RxObservable.throw(localize("error.login")); 153 | } 154 | return this.delete(`/articles/${slug}/comments/${commentId}`); 155 | } 156 | 157 | /** 158 | * 159 | */ 160 | public getTags(): RxObservable { 161 | return this.get("/tags").pipe(map((data: any) => data.tags), catchError(this.handleError)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/service/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Observable as RxObservable } from "rxjs"; 3 | import { tap, map, catchError } from "rxjs/operators"; 4 | import { HttpClient, HttpHeaders, HttpResponse, HttpErrorResponse } from "@angular/common/http"; 5 | import { setString, getString } from "application-settings"; 6 | import { User } from "~/model/User"; 7 | import { AbstractHttpService } from "~/service/AbstractHttpService"; 8 | import { Profile } from "~/model/Profile"; 9 | import { localize } from "nativescript-localize"; 10 | 11 | /** 12 | * 13 | */ 14 | @Injectable() 15 | export class UserService extends AbstractHttpService { 16 | /** */ 17 | private user: User; 18 | 19 | /** 20 | * 21 | * @param http 22 | */ 23 | constructor(protected http: HttpClient) { 24 | super(http); 25 | } 26 | 27 | /** 28 | * 29 | * @param email 30 | * @param password 31 | */ 32 | public login(email: string, password: string) { 33 | return this.pipeUser( 34 | this.post("/users/login", { 35 | user: { 36 | email, 37 | password 38 | } 39 | }) 40 | ); 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | public logout() { 47 | UserService.Token = ""; 48 | this.user = null; 49 | return true; 50 | } 51 | 52 | /** 53 | * 54 | * @param username 55 | * @param email 56 | * @param password 57 | */ 58 | public register(username: string, email: string, password: string) { 59 | return this.pipeUser( 60 | this.post("/users", { 61 | user: { 62 | username, 63 | email, 64 | password 65 | } 66 | }) 67 | ); 68 | } 69 | 70 | /** 71 | * 72 | */ 73 | public getUser() { 74 | return this.user; 75 | } 76 | 77 | /** 78 | * 79 | */ 80 | protected set User(user: User) { 81 | UserService.Token = user.token; 82 | this.user = user; 83 | } 84 | 85 | /** 86 | * 87 | */ 88 | public getCurrentUser() { 89 | if (!UserService.IsLoggedIn()) { 90 | return RxObservable.throw(localize("error.login")); 91 | } 92 | return this.pipeUser(this.get("/user")); 93 | } 94 | 95 | /** 96 | * 97 | * @param user The updated user object 98 | */ 99 | public updateUser(user: User) { 100 | if (!UserService.IsLoggedIn()) { 101 | return RxObservable.throw(localize("error.login")); 102 | } 103 | return this.pipeUser( 104 | this.put("/user", { 105 | user: { 106 | email: user.email, 107 | username: user.username, 108 | image: user.image, 109 | bio: user.bio 110 | } 111 | }) 112 | ); 113 | } 114 | 115 | /** 116 | * 117 | * @param password 118 | */ 119 | public updatePassword(password: string) { 120 | if (!UserService.IsLoggedIn()) { 121 | return RxObservable.throw(localize("error.login")); 122 | } 123 | return this.pipeUser( 124 | this.put("/user", { 125 | user: { 126 | password 127 | } 128 | }) 129 | ); 130 | } 131 | 132 | /** 133 | * 134 | * @param observable 135 | */ 136 | protected pipeUser(observable: RxObservable): RxObservable { 137 | return observable.pipe(map((data: any) => data.user), tap((user: User) => (this.User = user)), catchError(this.handleError)); 138 | } 139 | 140 | /** 141 | * 142 | * @param username 143 | */ 144 | public getProfile(username: string) { 145 | return this.get(`/profiles/${username}`).pipe(map((data: any) => data.profile), catchError(this.handleError)); 146 | } 147 | 148 | /** 149 | * 150 | * @param username 151 | * @param follow true if to follow, false if to unfollow 152 | */ 153 | public followUser(username: string, follow: boolean = true) { 154 | if (!UserService.IsLoggedIn()) { 155 | return RxObservable.throw(localize("error.login")); 156 | } 157 | let url: string = `/profiles/${username}/follow`; 158 | let request; 159 | if (follow) { 160 | request = this.post(url); 161 | } else { 162 | request = this.delete(url); 163 | } 164 | return request.pipe(map((data: any) => data.profile), catchError(this.handleError)); 165 | } 166 | 167 | /** 168 | * 169 | */ 170 | public static IsLoggedIn(): boolean { 171 | return !!getString("token"); 172 | } 173 | 174 | /** 175 | * 176 | */ 177 | public static get Token(): string { 178 | return getString("token", null); 179 | } 180 | 181 | /** 182 | * 183 | */ 184 | public static set Token(token: string) { 185 | setString("token", token); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/service/service.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders, Optional, SkipSelf } from "@angular/core"; 2 | import { ConduitService } from "~/service/ConduitService"; 3 | import { UserService } from "~/service/UserService"; 4 | 5 | @NgModule({ 6 | providers: [ConduitService] 7 | }) 8 | export class ServiceModule { 9 | /** 10 | * 11 | * @param parentModule 12 | */ 13 | constructor( 14 | @Optional() 15 | @SkipSelf() 16 | parentModule: ServiceModule 17 | ) { 18 | if (parentModule) { 19 | throw new Error("ServiceModule is already loaded. Import it in the AppModule only"); 20 | } 21 | } 22 | /** 23 | * 24 | */ 25 | public static forRoot(): ModuleWithProviders { 26 | return { 27 | ngModule: ServiceModule, 28 | providers: [UserService] 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/util/StatusBar.ts: -------------------------------------------------------------------------------- 1 | import * as application from "application"; 2 | import * as platform from "platform"; 3 | import * as utils from "utils/utils"; 4 | 5 | declare var android: any; 6 | declare var UIStatusBarStyle: any; 7 | declare var UIApplication: any; 8 | 9 | export function setStatusBarColors() { 10 | // Make the iOS status bar transparent with white text. 11 | if (application.ios) { 12 | application.on("launch", () => { 13 | utils.ios.getter(UIApplication, UIApplication.sharedApplication).statusBarStyle = UIStatusBarStyle.LightContent; 14 | }); 15 | } 16 | 17 | // Make the Android status bar transparent. 18 | // See http://bradmartin.net/2016/03/10/fullscreen-and-navigation-bar-color-in-a-nativescript-android-app/ 19 | // for details on the technique used. 20 | if (application.android && platform.device.sdkVersion >= "21") { 21 | application.android.on("activityStarted", () => { 22 | const View = android.view.View; 23 | const window = application.android.startActivity.getWindow(); 24 | window.setStatusBarColor(0x000000); 25 | 26 | const decorView = window.getDecorView(); 27 | decorView.setSystemUiVisibility( 28 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 29 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 30 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 31 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /app/vendor-platform.android.ts: -------------------------------------------------------------------------------- 1 | require("application"); 2 | if (!global["__snapshot"]) { 3 | // In case snapshot generation is enabled these modules will get into the bundle 4 | // but will not be required/evaluated. 5 | // The snapshot webpack plugin will add them to the tns-java-classes.js bundle file. 6 | // This way, they will be evaluated on app start as early as possible. 7 | require("ui/frame"); 8 | require("ui/frame/activity"); 9 | } 10 | -------------------------------------------------------------------------------- /app/vendor-platform.ios.ts: -------------------------------------------------------------------------------- 1 | // There is a bug in angular: https://github.com/angular/angular-cli/pull/8589/files 2 | // Legendary stuff, its webpack plugin pretty much doesn't work with empty TypeScript files in v1.8.3 3 | void 0; 4 | -------------------------------------------------------------------------------- /app/vendor.ts: -------------------------------------------------------------------------------- 1 | // Snapshot the ~/app.css and the theme 2 | const application = require("application"); 3 | require("ui/styling/style-scope"); 4 | const appCssContext = require.context("~/", false, /^\.\/app\.(css|scss|less|sass)$/); 5 | global.registerWebpackModules(appCssContext); 6 | application.loadAppCss(); 7 | 8 | require("./vendor-platform"); 9 | 10 | require("reflect-metadata"); 11 | require("@angular/platform-browser"); 12 | require("@angular/core"); 13 | require("@angular/common"); 14 | require("@angular/forms"); 15 | require("@angular/http"); 16 | require("@angular/router"); 17 | 18 | require("nativescript-angular/platform-static"); 19 | require("nativescript-angular/forms"); 20 | require("nativescript-angular/router"); 21 | -------------------------------------------------------------------------------- /assets/feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/feature.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/icon.png -------------------------------------------------------------------------------- /assets/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/screenshot_1.png -------------------------------------------------------------------------------- /assets/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/screenshot_2.png -------------------------------------------------------------------------------- /assets/screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/screenshot_3.png -------------------------------------------------------------------------------- /assets/screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/assets/screenshot_4.png -------------------------------------------------------------------------------- /e2e/conduit.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppiumDriver, createDriver, SearchOptions, Direction } from "nativescript-dev-appium"; 2 | import { assert } from "chai"; 3 | import { Home } from "./home"; 4 | 5 | /** 6 | * 7 | */ 8 | describe("Conduit", () => { 9 | /** */ 10 | let driver: AppiumDriver; 11 | /** */ 12 | let home: Home; 13 | 14 | /** 15 | * 16 | */ 17 | before(async () => { 18 | driver = await createDriver(); 19 | home = new Home(driver); 20 | }); 21 | 22 | /** 23 | * 24 | */ 25 | afterEach(async function() { 26 | if (this.currentTest.state === "failed") { 27 | await driver.logPageSource(this.currentTest.title); 28 | await driver.logScreenshot(this.currentTest.title); 29 | } 30 | await driver.navBack(); 31 | }); 32 | 33 | /** 34 | * 35 | */ 36 | after(async () => { 37 | await driver.quit(); 38 | }); 39 | 40 | /** 41 | * 42 | */ 43 | it("should load articles and tap on an author", async () => { 44 | await home.loaded(); 45 | await home.tapOnAuthor(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /e2e/config/appium.capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "android19": { 3 | "platformName": "Android", 4 | "platformVersion": "4.4", 5 | "deviceName": "Emulator-Api19-Default", 6 | "avd": "Emulator-Api19-Default", 7 | "lt": 60000, 8 | "newCommandTimeout": 720, 9 | "noReset": true, 10 | "fullReset": false, 11 | "app": "" 12 | }, 13 | "android21": { 14 | "platformName": "Android", 15 | "platformVersion": "5.0", 16 | "deviceName": "Emulator-Api21-Default", 17 | "avd": "Emulator-Api21-Default", 18 | "lt": 60000, 19 | "newCommandTimeout": 720, 20 | "noReset": true, 21 | "fullReset": false, 22 | "app": "" 23 | }, 24 | "android23": { 25 | "platformName": "Android", 26 | "platformVersion": "6.0", 27 | "deviceName": "Pixel_2_API_23", 28 | "avd": "Pixel_2_API_23", 29 | "lt": 60000, 30 | "newCommandTimeout": 720, 31 | "noReset": true, 32 | "fullReset": false, 33 | "app": "" 34 | }, 35 | "android24": { 36 | "platformName": "Android", 37 | "platformVersion": "7.0", 38 | "deviceName": "Emulator-Api24-Default", 39 | "avd": "Emulator-Api24-Default", 40 | "lt": 60000, 41 | "newCommandTimeout": 720, 42 | "noReset": true, 43 | "fullReset": false, 44 | "app": "" 45 | }, 46 | "android25": { 47 | "platformName": "Android", 48 | "platformVersion": "7.1", 49 | "deviceName": "Emulator-Api25-Google", 50 | "avd": "Emulator-Api25-Google", 51 | "lt": 60000, 52 | "newCommandTimeout": 720, 53 | "noReset": true, 54 | "fullReset": false, 55 | "app": "" 56 | }, 57 | "android26": { 58 | "platformName": "Android", 59 | "platformVersion": "8.0", 60 | "deviceName": "Emulator-Api26-Google", 61 | "avd": "Emulator-Api26-Google", 62 | "lt": 60000, 63 | "newCommandTimeout": 720, 64 | "noReset": true, 65 | "fullReset": false, 66 | "app": "" 67 | }, 68 | "android27": { 69 | "platformName": "Android", 70 | "platformVersion": "27", 71 | "deviceName": "Emulator-Api27-Google", 72 | "avd": "Emulator-Api27-Google", 73 | "lt": 60000, 74 | "newCommandTimeout": 720, 75 | "noReset": true, 76 | "fullReset": false, 77 | "app": "" 78 | }, 79 | "sim.iPhone7.iOS100": { 80 | "platformName": "iOS", 81 | "platformVersion": "10.0", 82 | "deviceName": "iPhone 7 100", 83 | "noReset": true, 84 | "fullReset": false, 85 | "app": "" 86 | }, 87 | "sim.iPhone8.iOS110": { 88 | "platformName": "iOS", 89 | "platformVersion": "11.3", 90 | "deviceName": "iPhone 8 1130", 91 | "noReset": true, 92 | "fullReset": false, 93 | "app": "" 94 | }, 95 | "sim.iPhoneX.iOS110": { 96 | "platformName": "iOS", 97 | "platformVersion": "11.3", 98 | "deviceName": "iPhone X", 99 | "noReset": true, 100 | "fullReset": false, 101 | "app": "" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /e2e/config/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 999999 2 | --recursive e2e 3 | --reporter mocha-multi 4 | --reporter-options spec=-,mocha-junit-reporter=test-results.xml -------------------------------------------------------------------------------- /e2e/home.ts: -------------------------------------------------------------------------------- 1 | import { AppiumDriver, Direction, SearchOptions, UIElement } from "nativescript-dev-appium"; 2 | import { assert } from "chai"; 3 | 4 | /** 5 | * 6 | */ 7 | export class Home { 8 | /** 9 | * 10 | * @param driver 11 | */ 12 | constructor(private driver: AppiumDriver) {} 13 | 14 | /** 15 | * 16 | */ 17 | public loaded = async () => { 18 | const lblTitle = await this.driver.findElementByText("Conduit"); 19 | assert.isTrue(await lblTitle.isDisplayed()); 20 | console.log("Conduit loaded!"); 21 | }; 22 | 23 | /** 24 | * 25 | */ 26 | public tapOnAuthor = async () => { 27 | const author: UIElement = await this.driver.findElementByAccessibilityId('article-item-author-username'); 28 | const username = await author.text(); 29 | const authorElement: UIElement = await this.driver.findElementByAccessibilityId('article-item-author'); 30 | await author.tap(); 31 | const authorUsername = await this.driver.findElementByText(username); 32 | assert.isTrue(await authorUsername.isDisplayed()); 33 | console.log("Author '" + username + "' loaded!"); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /e2e/setup.ts: -------------------------------------------------------------------------------- 1 | import { startServer, stopServer } from "nativescript-dev-appium"; 2 | 3 | before("start server", async () => { 4 | await startServer(); 5 | }); 6 | 7 | after("stop server", async () => { 8 | await stopServer(); 9 | }); 10 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "importHelpers": false, 8 | "types": [ 9 | "node", 10 | "mocha", 11 | "chai" 12 | ], 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea/nativescript-realworld-example-app/eb932b7b015b9e4ae0af344c01bb27cca837dd1e/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.insanitydesign.nativescriptrealworldexampleapp", 3 | "description": "Conduit - A NativeScript RealWorld Example App", 4 | "version": "1.1.1", 5 | "license": "MIT", 6 | "author": "Savas Ziplies", 7 | "displayName": "Conduit", 8 | "readme": "See how a Medium.com clone (called Conduit) is built using NativeScript to connect to any other backend from https://realworld.io/.", 9 | "repository": "https://github.com/nea/nativescript-realworld-example-app", 10 | "nativescript": { 11 | "id": "com.insanitydesign.nativescriptrealworldexampleapp", 12 | "tns-android": { 13 | "version": "4.1.1" 14 | }, 15 | "tns-ios": { 16 | "version": "4.1.0" 17 | } 18 | }, 19 | "dependencies": { 20 | "@angular/animations": "~6.0.0", 21 | "@angular/common": "~6.0.0", 22 | "@angular/compiler": "~6.0.0", 23 | "@angular/core": "~6.0.0", 24 | "@angular/forms": "~6.0.0", 25 | "@angular/http": "~6.0.0", 26 | "@angular/platform-browser": "~6.0.0", 27 | "@angular/platform-browser-dynamic": "~6.0.0", 28 | "@angular/router": "~6.0.0", 29 | "@types/validator": "^9.4.1", 30 | "markdown": "^0.5.0", 31 | "nativescript-angular": "^6.0.0", 32 | "nativescript-feedback": "^1.2.0", 33 | "nativescript-floatingactionbutton": "^4.1.3", 34 | "nativescript-localize": "^3.0.3", 35 | "nativescript-ngx-fonticon": "^4.2.0", 36 | "nativescript-social-share": "^1.5.0", 37 | "nativescript-theme-core": "~1.0.4", 38 | "nativescript-ui-autocomplete": "^3.7.1", 39 | "nativescript-ui-dataform": "^3.6.0", 40 | "nativescript-ui-listview": "^3.5.7", 41 | "nativescript-ui-sidedrawer": "^4.1.1", 42 | "reflect-metadata": "~0.1.10", 43 | "rxjs": "~6.0.0 || >=6.1.0", 44 | "tns-core-modules": "^4.1.0", 45 | "validator": "^10.2.0", 46 | "zone.js": "^0.8.26" 47 | }, 48 | "devDependencies": { 49 | "@angular-devkit/core": "~0.6.3", 50 | "@angular/compiler-cli": "~6.0.0", 51 | "@ngtools/webpack": "~6.0.3", 52 | "@types/chai": "^4.0.2", 53 | "@types/mocha": "^2.2.41", 54 | "@types/node": "^7.0.5", 55 | "babel-traverse": "6.4.5", 56 | "babel-types": "6.4.5", 57 | "babylon": "6.4.5", 58 | "clean-webpack-plugin": "~0.1.19", 59 | "copy-webpack-plugin": "~4.5.1", 60 | "css-loader": "~0.28.11", 61 | "extract-text-webpack-plugin": "~3.0.2", 62 | "lazy": "1.0.11", 63 | "mobile-devices-controller": "^2.4.8", 64 | "nativescript-dev-appium": "^3.3.0", 65 | "nativescript-dev-sass": "^1.5.0", 66 | "nativescript-dev-typescript": "~0.7.0", 67 | "nativescript-dev-webpack": "^0.12.0", 68 | "nativescript-worker-loader": "~0.9.0", 69 | "raw-loader": "~0.5.1", 70 | "resolve-url-loader": "~2.3.0", 71 | "sass-loader": "~7.0.1", 72 | "typescript": "~2.7.2", 73 | "uglifyjs-webpack-plugin": "~1.2.5", 74 | "webpack": "~4.6.0", 75 | "webpack-bundle-analyzer": "~2.13.0", 76 | "webpack-cli": "~2.1.3", 77 | "webpack-sources": "~1.1.0" 78 | }, 79 | "scripts": { 80 | "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts", 81 | "e2e-watch": "tsc -p e2e --watch" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld Example App](logo.png) 2 | > ### NativeScript codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld) spec and API. 3 | 4 | Get it on Google Play 5 | ![iOS](https://img.shields.io/badge/iOS-upcoming-yellow.svg) 6 | [![GitHub license](https://img.shields.io/github/license/nea/nativescript-realworld-example-app.svg)](https://github.com/nea/nativescript-realworld-example-app/blob/master/LICENSE.md) 7 | [![Twitter](https://img.shields.io/twitter/url/https/github.com/nea/nativescript-realworld-example-app.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fnea%2Fnativescript-realworld-example-app) 8 | 9 | This codebase was created to demonstrate a fully fledged fullstack application built with **NativeScript** including CRUD operations, authentication, routing, pagination, and more. 10 | 11 | See how a Medium.com clone (called Conduit) is built using NativeScript to connect to any other backend from https://realworld.io/. 12 | 13 | For more information on how to this works with other backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 14 | 15 | I've gone to great lengths to adhere to the **NativeScript** community styleguides & best practices but had to adapt between the RealWorld specification and general mobile layout of Medium.com. 16 | 17 | ## Getting started 18 | It is assumed that you have installed and configured NativeScript properly. If not, head to https://docs.nativescript.org/start/quick-setup and validate its correct functionality. 19 | 20 | To start the emulator with this repository: 21 | > `git clone https://github.com/nea/nativescript-realworld-example-app.git` 22 | > `cd nativescript-realworld-example-app` 23 | > `tns run android` or `tns run ios` 24 | 25 | ### Development 26 | This project has been developed with [Visual Studio Code](https://code.visualstudio.com/) and the [NativeScript extension](https://www.nativescript.org/nativescript-for-visual-studio-code). It has been tested and live-synced with a local Pixel 2 android23 emulator for the most time. 27 | 28 | ## How it works 29 | This app works as a NativeScript real-world showcase and is based on [NativeScript 4](https://nativescript.org) Angular/TypeScript style. 30 | 31 | Head over to the [NativeScript Docs](https://docs.nativescript.org/angular/start/introduction) to find out how to get started with NativeScript, Angular and Typescript. 32 | 33 | ### Concepts 34 | This RealWorld app tries to show the following NativeScript concepts: 35 | * i18n 36 | * Custom Component inclusion 37 | * SideDrawer Menu 38 | * Services 39 | * Application Settings 40 | * Custom ActionBar 41 | * Lazy-loading 42 | * Modal Dialogs 43 | * Navigation 44 | * Plugins (NativeScript and npm JavaScript) 45 | * Sass 46 | * TTF inclusion 47 | * e2e testing 48 | 49 | To show as many concepts as possible the structure is not necessarily consistent but may differ between views. This example app does not claim best-practice or anything similar but just one way of achieving a result. 50 | 51 | ### Architecture 52 | The project follows the general NativeScript/Angular structure without any specifics. It uses lazy-loaded modules to encapsulate functionality further. It uses frame and router navigation to go back and forth between pages. 53 | 54 | The project itself is mainly located in the `app/` folder. It follows this general architecture: 55 | * `module/` contains the different views and according logic, split into a general, lazy-loaded module structure resembling the UIs 56 | * `service/` contains shared services used to encapsulated global, view-independent logic, i.e. the backend calls 57 | * `model/` contains shared entity classes used as models throughout the other files 58 | * `fonts/` contains [FontAwesome](https://fontawesome.com/v4.7.0/) icons used in the app. See [nativescript-ngx-fonticon](https://market.nativescript.org/plugins/nativescript-ngx-fonticon) for more information 59 | * `i18n/` contains the translation files. See [nativescript-localize](https://market.nativescript.org/plugins/nativescript-localize) for more information 60 | 61 | #### Files 62 | Each component comes in two parts: 63 | * `xyz.component.ts` the source 64 | * `xyz.component.html` the template 65 | 66 | Everything is loaded in their according modules and reached via module-specific routing files: 67 | * `x.module.ts` the general module 68 | * `x.routing.ts` the routing file 69 | * `x.css` according CSS 70 | 71 | Not all files are necessarily needed to be encapsulated in such a granularity, but the structure was executed through the source to stay consistent. 72 | 73 | ### Frontend 74 | This repository orientated on the frontend instructions but adapted to some specifics and based all styling and routing on a mix of the Medium.com app and the overall RealWorld StarterKit instructions. 75 | 76 | #### Styles 77 | The UI is composed based on the Medium.com app. If information was missing/different between Conduit and Medium.com a mixed adaptation has been implemented. 78 | 79 | #### Routing 80 | Nearly all routes have been adapted one-to-one into the app. Some differences occur such as `/home`. 81 | 82 | #### Screenshots 83 | 84 | 85 | 86 | 87 | 88 | ### Other Backends 89 | Obviously, this RealWorld app is a frontend app. But it can connect to all backends implementing the [RealWorld](https://github.com/gothinkster/realworld) spec and API. To test you own backend implementation just change the URL in the settings dialog. 90 | 91 | ### Plugins 92 | This example app uses a set of available NativeScript plugins to visualize the possible usage. Head over to the [NativeScript Market](https://market.nativescript.org/) for more information. 93 | 94 | Used NativeScript plugins from https://market.nativescript.org: 95 | * [nativescript-feedback](https://market.nativescript.org/plugins/nativescript-feedback) to show general, fancy messages 96 | * [nativescript-floatingactionbutton](https://market.nativescript.org/plugins/nativescript-floatingactionbutton) to add new articles 97 | * [nativescript-localize](https://market.nativescript.org/plugins/nativescript-localize) to localize the static text 98 | * [nativescript-ngx-fonticon](https://market.nativescript.org/plugins/nativescript-ngx-fonticon) to include [FontAwesome](https://fontawesome.com/) icons in menus 99 | * [nativescript-ui-listview](https://market.nativescript.org/plugins/nativescript-ui-listview) to present the articles 100 | * [nativescript-ui-sidedrawer](https://market.nativescript.org/plugins/nativescript-ui-sidedrawer) to add a side-menu 101 | * [nativescript-ui-dataform](https://market.nativescript.org/plugins/nativescript-ui-dataform) to create and edit articles 102 | * [nativescript-ui-autocomplete](https://market.nativescript.org/plugins/nativescript-ui-autocomplete) for the editor tag fields 103 | * [nativescript-social-share](https://market.nativescript.org/plugins/nativescript-social-share) for article sharing 104 | * [nativescript-dev-appium](https://market.nativescript.org/plugins/nativescript-dev-appium) for e2e Appium tests 105 | 106 | Other NPM plugins: 107 | * [markdown-js](https://github.com/evilstreak/markdown-js) for article markdown body rendering 108 | * [validator](https://github.com/chriso/validator.js) for Email and URL validation 109 | 110 | ## Testing 111 | This project has been manually tested against 112 | * Emulator 113 | * Pixel 2 Android SDK 23 114 | * Devices 115 | * Samsung S8 Android 8.0.0 116 | 117 | ### Automated tests 118 | The project contains an example e2e test to illustrate an end-to-end test case. 119 | 120 | ## License & Credits 121 | Credits have to go out to [Thinkster](https://thinkster.io/) with their awesome [RealWorld](https://github.com/gothinkster/realworld) idea as well as [NativeScript](https://www.nativescript.org/). 122 | 123 | Thanks to all the plugin developers and articles by so many people on the NativeScript blog, forums and https://www.thepolyglotdeveloper.com/. Big thanks! 124 | 125 | This project is licensed under the MIT license. 126 | 127 | ## Disclaimer 128 | This source and the whole package comes without warranty. It may or may not harm your computer or cell phone. Please use with care. Any damage cannot be related back to the author. The source has been tested on a virtual environment and scanned for viruses and has passed all tests. 129 | 130 | ## Personal Note 131 | *I don't know if this is very useful for a lot of people but I wanted a real-world tutorial with NativeScript, so here we are :) I hope this proves helpful to you... with all its Bugs and Issues ;) If you like it you can give me a shout at [INsanityDesign](https://insanitydesign.com) or let me know via this repository.* 132 | -------------------------------------------------------------------------------- /references.d.ts: -------------------------------------------------------------------------------- 1 | /// Needed for autocompletion and compilation. -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "moduleResolution": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "noEmitHelpers": true, 8 | "noEmitOnError": true, 9 | "lib": [ 10 | "es6", 11 | "dom", 12 | "es2015.iterable" 13 | ], 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": [ 17 | "app/*" 18 | ], 19 | "*": [ 20 | "./node_modules/tns-core-modules/*", 21 | "./node_modules/*" 22 | ] 23 | } 24 | }, 25 | "exclude": [ 26 | "node_modules", 27 | "platforms" 28 | ] 29 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [], 4 | "rules": { 5 | "max-line-length": { 6 | "options": [140] 7 | }, 8 | "object-literal-sort-keys": false, 9 | "comment-format": false, 10 | "trailing-comma": false, 11 | "array-type": false, 12 | "curly": false, 13 | "object-literal-key-quotes": false, 14 | "ban-types": false, 15 | "ordered-imports": false, 16 | "member-ordering": false, 17 | "no-angle-bracket-type-assertion": false, 18 | "whitespace": false, 19 | "prefer-const": false, 20 | "arrow-parens": false, 21 | "new-parens": true, 22 | "interface-name": false, 23 | "variable-name": false, 24 | "quotemark": false, 25 | "only-arrow-functions": false, 26 | "interface-over-type-literal": false, 27 | "no-arg": true, 28 | "no-bitwise": false, 29 | "no-conditional-assignment": true, 30 | "no-consecutive-blank-lines": false, 31 | "no-console": false, 32 | "no-string-literal": false, 33 | "no-empty": false, 34 | "semicolon": false 35 | }, 36 | "jsRules": { 37 | "max-line-length": { 38 | "options": [140] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join, relative, resolve, sep } = require("path"); 2 | 3 | const webpack = require("webpack"); 4 | const nsWebpack = require("nativescript-dev-webpack"); 5 | const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); 6 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 7 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 8 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 9 | const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin"); 10 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 11 | 12 | module.exports = env => { 13 | // Add your custom Activities, Services and other Android app components here. 14 | const appComponents = [ 15 | "tns-core-modules/ui/frame", 16 | "tns-core-modules/ui/frame/activity", 17 | ]; 18 | 19 | const platform = env && (env.android && "android" || env.ios && "ios"); 20 | if (!platform) { 21 | throw new Error("You need to provide a target platform!"); 22 | } 23 | 24 | const platforms = ["ios", "android"]; 25 | const projectRoot = __dirname; 26 | nsWebpack.loadAdditionalPlugins({ projectDir: projectRoot }); 27 | 28 | // Default destination inside platforms//... 29 | const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); 30 | const appResourcesPlatformDir = platform === "android" ? "Android" : "iOS"; 31 | 32 | const { 33 | // The 'appPath' and 'appResourcesPath' values are fetched from 34 | // the nsconfig.json configuration file 35 | // when bundling with `tns run android|ios --bundle`. 36 | appPath = "app", 37 | appResourcesPath = "app/App_Resources", 38 | 39 | // You can provide the following flags when running 'tns run android|ios' 40 | aot, // --env.aot 41 | snapshot, // --env.snapshot 42 | uglify, // --env.uglify 43 | report, // --env.report 44 | } = env; 45 | 46 | const appFullPath = resolve(projectRoot, appPath); 47 | const appResourcesFullPath = resolve(projectRoot, appResourcesPath); 48 | 49 | const entryModule = aot ? 50 | nsWebpack.getAotEntryModule(appFullPath) : 51 | `${nsWebpack.getEntryModule(appFullPath)}.ts`; 52 | const entryPath = `.${sep}${entryModule}`; 53 | 54 | const config = { 55 | mode: uglify ? "production" : "development", 56 | context: appFullPath, 57 | watchOptions: { 58 | ignored: [ 59 | appResourcesFullPath, 60 | // Don't watch hidden files 61 | "**/.*", 62 | ] 63 | }, 64 | target: nativescriptTarget, 65 | entry: { 66 | bundle: entryPath, 67 | }, 68 | output: { 69 | pathinfo: false, 70 | path: dist, 71 | libraryTarget: "commonjs2", 72 | filename: "[name].js", 73 | globalObject: "global", 74 | }, 75 | resolve: { 76 | extensions: [".ts", ".js", ".scss", ".css"], 77 | // Resolve {N} system modules from tns-core-modules 78 | modules: [ 79 | resolve(__dirname, "node_modules/tns-core-modules"), 80 | resolve(__dirname, "node_modules"), 81 | "node_modules/tns-core-modules", 82 | "node_modules", 83 | ], 84 | alias: { 85 | '~': appFullPath 86 | }, 87 | symlinks: true 88 | }, 89 | resolveLoader: { 90 | symlinks: false 91 | }, 92 | node: { 93 | // Disable node shims that conflict with NativeScript 94 | "http": false, 95 | "timers": false, 96 | "setImmediate": false, 97 | "fs": "empty", 98 | "__dirname": false, 99 | }, 100 | devtool: "none", 101 | optimization: { 102 | splitChunks: { 103 | cacheGroups: { 104 | vendor: { 105 | name: "vendor", 106 | chunks: "all", 107 | test: (module, chunks) => { 108 | const moduleName = module.nameForCondition ? module.nameForCondition() : ''; 109 | return /[\\/]node_modules[\\/]/.test(moduleName) || 110 | appComponents.some(comp => comp === moduleName); 111 | }, 112 | enforce: true, 113 | }, 114 | } 115 | }, 116 | minimize: !!uglify, 117 | minimizer: [ 118 | new UglifyJsPlugin({ 119 | uglifyOptions: { 120 | parallel: true, 121 | cache: true, 122 | output: { 123 | comments: false, 124 | }, 125 | compress: { 126 | // The Android SBG has problems parsing the output 127 | // when these options are enabled 128 | 'collapse_vars': platform !== "android", 129 | sequences: platform !== "android", 130 | } 131 | } 132 | }) 133 | ], 134 | }, 135 | module: { 136 | rules: [ 137 | { 138 | test: new RegExp(entryPath), 139 | use: [ 140 | // Require all Android app components 141 | platform === "android" && { 142 | loader: "nativescript-dev-webpack/android-app-components-loader", 143 | options: { modules: appComponents } 144 | }, 145 | 146 | { 147 | loader: "nativescript-dev-webpack/bundle-config-loader", 148 | options: { 149 | registerPages: false, 150 | loadCss: !snapshot, // load the application css if in debug mode 151 | } 152 | }, 153 | ].filter(loader => !!loader) 154 | }, 155 | 156 | { test: /\.html$|\.xml$/, use: "raw-loader" }, 157 | 158 | // tns-core-modules reads the app.css and its imports using css-loader 159 | { 160 | test: /[\/|\\]app\.css$/, 161 | use: { 162 | loader: "css-loader", 163 | options: { minimize: false, url: false }, 164 | } 165 | }, 166 | { 167 | test: /[\/|\\]app\.scss$/, 168 | use: [ 169 | { loader: "css-loader", options: { minimize: false, url: false } }, 170 | "sass-loader" 171 | ] 172 | }, 173 | 174 | // Angular components reference css files and their imports using raw-loader 175 | { test: /\.css$/, exclude: /[\/|\\]app\.css$/, use: "raw-loader" }, 176 | { test: /\.scss$/, exclude: /[\/|\\]app\.scss$/, use: ["raw-loader", "resolve-url-loader", "sass-loader"] }, 177 | 178 | // Compile TypeScript files with ahead-of-time compiler. 179 | { 180 | test: /.ts$/, use: [ 181 | "nativescript-dev-webpack/moduleid-compat-loader", 182 | "@ngtools/webpack", 183 | ] 184 | }, 185 | 186 | // Mark files inside `@angular/core` as using SystemJS style dynamic imports. 187 | // Removing this will cause deprecation warnings to appear. 188 | { 189 | test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, 190 | parser: { system: true }, 191 | }, 192 | ], 193 | }, 194 | plugins: [ 195 | // Define useful constants like TNS_WEBPACK 196 | new webpack.DefinePlugin({ 197 | "global.TNS_WEBPACK": "true", 198 | }), 199 | // Remove all files from the out dir. 200 | new CleanWebpackPlugin([ `${dist}/**/*` ]), 201 | // Copy native app resources to out dir. 202 | new CopyWebpackPlugin([ 203 | { 204 | from: `${appResourcesFullPath}/${appResourcesPlatformDir}`, 205 | to: `${dist}/App_Resources/${appResourcesPlatformDir}`, 206 | context: projectRoot 207 | }, 208 | ]), 209 | // Copy assets to out dir. Add your own globs as needed. 210 | new CopyWebpackPlugin([ 211 | { from: "fonts/**" }, 212 | { from: "**/*.jpg" }, 213 | { from: "**/*.png" }, 214 | ], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }), 215 | // Generate a bundle starter script and activate it in package.json 216 | new nsWebpack.GenerateBundleStarterPlugin([ 217 | "./vendor", 218 | "./bundle", 219 | ]), 220 | // For instructions on how to set up workers with webpack 221 | // check out https://github.com/nativescript/worker-loader 222 | new NativeScriptWorkerPlugin(), 223 | // AngularCompilerPlugin with augmented NativeScript filesystem to handle platform specific resource resolution. 224 | new nsWebpack.NativeScriptAngularCompilerPlugin({ 225 | entryModule: resolve(appPath, "app.module#AppModule"), 226 | tsConfigPath: join(__dirname, "tsconfig.esm.json"), 227 | skipCodeGeneration: !aot, 228 | platformOptions: { 229 | platform, 230 | platforms, 231 | }, 232 | }), 233 | // Does IPC communication with the {N} CLI to notify events when running in watch mode. 234 | new nsWebpack.WatchStateLoggerPlugin(), 235 | ], 236 | }; 237 | 238 | if (report) { 239 | // Generate report files for bundles content 240 | config.plugins.push(new BundleAnalyzerPlugin({ 241 | analyzerMode: "static", 242 | openAnalyzer: false, 243 | generateStatsFile: true, 244 | reportFilename: resolve(projectRoot, "report", `report.html`), 245 | statsFilename: resolve(projectRoot, "report", `stats.json`), 246 | })); 247 | } 248 | 249 | if (snapshot) { 250 | config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ 251 | chunk: "vendor", 252 | requireModules: [ 253 | "reflect-metadata", 254 | "@angular/platform-browser", 255 | "@angular/core", 256 | "@angular/common", 257 | "@angular/router", 258 | "nativescript-angular/platform-static", 259 | "nativescript-angular/router", 260 | ], 261 | projectRoot, 262 | webpackConfig: config, 263 | })); 264 | } 265 | 266 | return config; 267 | }; 268 | --------------------------------------------------------------------------------