Skip to content

Commit

Permalink
feat: add basic create & edit
Browse files Browse the repository at this point in the history
refs: #9
  • Loading branch information
sungmin-park committed Nov 29, 2021
1 parent 510d7c6 commit b06eda6
Show file tree
Hide file tree
Showing 11 changed files with 526 additions and 94 deletions.
2 changes: 1 addition & 1 deletion backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach

dependencies {
// kenet
val kenetVersion = "9cc72ddcf7"
val kenetVersion = "619d409526"
implementation("com.github.kotlin-everywhere.kenet:kenet-server:$kenetVersion")
implementation("com.github.kotlin-everywhere.kenet:kenet-server-engine-http:$kenetVersion")
implementation("com.github.kotlin-everywhere.kenet:kenet-gen-typescript:$kenetVersion")
Expand Down
242 changes: 231 additions & 11 deletions backend/src/main/kotlin/org/kotlin/everywhere/realworld/api.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package org.kotlin.everywhere.realworld

import kotlinx.serialization.Serializable
import org.kotlin.everywhere.net.Call
import org.kotlin.everywhere.net.Kenet
import org.kotlin.everywhere.net.invoke
import java.util.*
import kotlin.reflect.full.findParameterByName
import kotlin.reflect.full.primaryConstructor

class Api : Kenet() {
val signUp by c<SignUpReq, SignUpRes>()
val signIn by c<SignInReq, SignInRes>()

// my pages
val basicUserInfo by c<BasicUserInfoReq, BasicUserInfoRes>()
val updateProfile by c<UpdateProfileReq, UpdateProfileRes>()

// articles
val articleCreate by c<ArticleCreateReq, ArticleCreateRes>()
val articleEditShow by c<ArticleEditShowReq, ArticleEditShowRes>()
val articleEdit by c<ArticleEditReq, ArticleEditRes>()

// index
val feedList by c<FeedListReq, FeedListRes>()
}

@Serializable
Expand All @@ -20,9 +34,6 @@ class SignUpRes(val errors: List<String> = listOf())
@Serializable
class SignInReq(val email: String, val password: String)

@Serializable
data class Res<T : Any>(val errors: List<String>, val data: T? = null)

@Serializable
class SignInRes(val errors: List<String> = listOf(), val data: Data? = null) {
@Serializable
Expand All @@ -35,6 +46,15 @@ class SignInRes(val errors: List<String> = listOf(), val data: Data? = null) {
)
}

@Serializable
class BasicUserInfoReq(val accessToken: String)

@Serializable
class BasicUserInfoRes(val data: Data? = null) {
@Serializable
class Data(val name: String, val email: String, val note: String, val profilePictureUrl: String)
}

interface AuthorizedReq {
val accessToken: String
}
Expand All @@ -43,12 +63,15 @@ interface ErrorsRes {
val errors: List<String>
}

inline fun <REQ : AuthorizedReq, reified RES : ErrorsRes> withUser(crossinline handler: (user: User, req: REQ) -> RES): (req: REQ) -> RES {
return { req ->
private inline fun <REQ : AuthorizedReq, reified RES : ErrorsRes> Call<REQ, RES>.withUser(crossinline handler: (user: User, req: REQ) -> RES) {
invoke { req ->
val user = users.firstOrNull { it.accessTokens.contains(req.accessToken) }
if (user == null) {
val constructors = RES::class.constructors
constructors.last().call(listOf("Invalid access token"))
val cons =
RES::class.primaryConstructor ?: throw IllegalArgumentException("cannot find primary constructor")
val errorsParameterName =
cons.findParameterByName("errors") ?: throw IllegalArgumentException("Cannot find errors parameter")
cons.callBy(mapOf(errorsParameterName to listOf("invalid access token")))
} else {
handler(user, req)
}
Expand All @@ -68,13 +91,74 @@ class UpdateProfileReq(
@Serializable
class UpdateProfileRes(override var errors: List<String> = listOf()) : ErrorsRes


@Serializable
class ArticleCreateReq(
override val accessToken: String,
val title: String,
val description: String,
val article: String,
val tags: String
) : AuthorizedReq

@Serializable
class ArticleCreateRes(override val errors: List<String> = listOf(), val slug: String? = null) : ErrorsRes

@Serializable
class ArticleEditShowReq(
override val accessToken: String,
val slug: String
) : AuthorizedReq

@Serializable
class ArticleEditShowRes(override val errors: List<String> = listOf(), val data: Data? = null) : ErrorsRes {
@Serializable
class Data(
val pk: Int,
val title: String,
val description: String,
val article: String,
val tags: List<String>
)
}

@Serializable
class ArticleEditReq(
override val accessToken: String,
val pk: Int,
val title: String,
val description: String,
val article: String,
val tags: String
) : AuthorizedReq

@Serializable
class ArticleEditRes(override val errors: List<String> = listOf(), val slug: String? = null) : ErrorsRes


@Serializable
class FeedListReq(val accessToken: String?)

@Serializable
class FeedListRes(val feeds: List<Feed>) {
@Serializable
class Feed(
val userName: String,
val userProfilePictureUrl: String,
val lastUpdatedAt: String,
val title: String,
val description: String,
val slug: String
)
}

fun Api.init() {
signUp { req ->
val emailTaken = users.any { it.email == req.email }
if (emailTaken) {
return@signUp SignUpRes(errors = listOf("That email is already taken"))
}
users.add(User(name = req.name, email = req.email, password = req.password))
users.add(User(pk = ++lastUserPk, name = req.name, email = req.email, password = req.password))
SignUpRes()
}

Expand All @@ -95,7 +179,19 @@ fun Api.init() {
)
}

updateProfile(withUser { user, req ->
basicUserInfo { req ->
val user =
users.firstOrNull { it.accessTokens.contains(req.accessToken) }
?: return@basicUserInfo BasicUserInfoRes()
BasicUserInfoRes(data = BasicUserInfoRes.Data(
name = user.name,
email = user.email,
note = user.note,
profilePictureUrl = user.profilePictureUrl
))
}

updateProfile.withUser { user, req ->
if (req.name.isBlank()) {
return@withUser UpdateProfileRes(errors = listOf("Input a name"))
}
Expand All @@ -112,10 +208,95 @@ fun Api.init() {
user.profilePictureUrl = req.profilePictureUrl

UpdateProfileRes()
})
}

articleCreate.withUser { user, req ->
if (req.title.isBlank()) {
return@withUser ArticleCreateRes(listOf("Input a title"))
}
if (req.article.isBlank()) {
return@withUser ArticleCreateRes(listOf("Input an article"))
}

val newArticle = Article(
pk = ++lastArticlePk,
slug = req.title.slugify(),
userPk = user.pk,
title = req.title,
description = req.description,
article = req.article,
tags = req.tags.split(",").map { it.trim() }
)
if (articles.any { it.slug == newArticle.slug }) {
// 만약 slug 가 사용중이면 pk 를 붙여 준다.
newArticle.slug = "${newArticle.slug}_${newArticle.pk}"
}
articles.add(newArticle)

ArticleCreateRes(slug = newArticle.slug)
}
articleEditShow.withUser { user, req ->
val article = articles.firstOrNull { it.slug == req.slug && it.userPk == user.pk }
?: return@withUser ArticleEditShowRes(errors = listOf("Invalid article"))
ArticleEditShowRes(
data = ArticleEditShowRes.Data(
pk = article.pk,
title = article.title,
description = article.description,
article = article.article,
tags = article.tags
)
)
}

articleEdit.withUser { user, req ->
if (req.title.isBlank()) {
return@withUser ArticleEditRes(errors = listOf("Input a title"))
}
if (req.article.isBlank()) {
return@withUser ArticleEditRes(errors = listOf("Input a article"))
}

val article = articles.firstOrNull { it.pk == req.pk && it.userPk == user.pk }
?: return@withUser ArticleEditRes(errors = listOf("Not found an article"))

article.slug = req.title.slugify()
if (articles.filter { it.pk != req.pk }.any { it.slug == article.slug }) {
// 만약 slug 가 사용중이면 pk 를 붙여 준다.
article.slug = "${article.slug}_${article.pk}"
}
article.title = req.title
article.description = req.description
article.article = req.article
article.tags = req.tags.split(",").map { it.trim() }
article.updatedAt = Date()

ArticleEditRes(slug = article.slug)
}

feedList { req ->
val user = users.firstOrNull { it.accessTokens.contains(req.accessToken) }
val feeds = articles
.filter { user == null || it.userPk == user.pk }
.map { article ->
val articleUser = users.firstOrNull { article.userPk == it.pk }
FeedListRes.Feed(
userName = articleUser?.name ?: "Unkown User",
userProfilePictureUrl = articleUser?.profilePictureUrl ?: "",
lastUpdatedAt = (article.updatedAt ?: article.createdAt).toString(),
title = article.title,
description = article.description,
slug = article.slug
)
}
FeedListRes(feeds = feeds)
}
}

var lastUserPk = 0

data class User(
val pk: Int,
var name: String,
val email: String,
var password: String,
Expand All @@ -124,4 +305,43 @@ data class User(
var profilePictureUrl: String = "",
)

val users = mutableListOf(User(name = "test", email = "[email protected]", password = "1234"))
private val users =
mutableListOf(User(pk = ++lastUserPk, name = "test", email = "[email protected]", password = "1234"))

private var lastArticlePk = 0

data class Article(
val pk: Int,
var slug: String,
val userPk: Int,
var title: String,
var description: String,
var article: String,
var tags: List<String>,
var createdAt: Date = Date(),
var updatedAt: Date? = null
)

private val articles =
mutableListOf(
Article(
pk = ++lastArticlePk,
slug = "first",
userPk = 1,
title = "First",
description = "Description",
article = "Article",
tags = listOf("One", "Two")
)
)


// TODO :: correct slug
fun String.slugify(): String =
this.lowercase()
.replace("-_".toRegex(), " ")
.split(' ')
.asSequence()
.map { it.trim() }
.filter { it.isNotBlank() }
.joinToString("-")
5 changes: 5 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@types/node": "^12.20.33",
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.9",
"classnames": "^2.3.1",
"mobx": "^6.3.8",
"mobx-react-lite": "^3.2.2",
"react": "^17.0.2",
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,8 @@ function App() {
<Route exact path="/register" component={SignUpPage} />
<Route exact path="/settings" component={SettingPage} />
<Route exact path="/editor" component={ArticleCreatePage} />
<Route exact path="/editor/slug" component={ArticleEditPage} />
<Route
exact
path="/article/article-slug-here"
component={ArticlePage}
/>
<Route exact path="/editor/:slug" component={ArticleEditPage} />
<Route exact path="/article/:slug" component={ArticlePage} />
<Route exact path="/profile/username" component={ProfilePage} />
<Route
exact
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/model.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import { makeAutoObservable } from "mobx";
import { api } from "./api";

export class SiteModel {
user: SiteUser | null = null;

constructor() {
makeAutoObservable(this);

const accessToken = localStorage.getItem("accessToken");
if (!accessToken?.length) {
return;
}

api.basicUserInfo({ accessToken }).then((res) => {
if (res.data) {
this.setUser({ ...res.data, accessToken });
} else {
this.setUser(null);
}
});
}

setUser(user: SiteUser) {
setUser(user: SiteUser | null) {
this.user = user;

if (user) {
localStorage.setItem("accessToken", user.accessToken);
} else {
localStorage.removeItem("accessToken");
}
}
}

Expand Down
Loading

0 comments on commit b06eda6

Please sign in to comment.