Skip to content
This repository has been archived by the owner on May 11, 2021. It is now read-only.

Commit

Permalink
Merge pull request #153 from igorkamyshev/custom-tip
Browse files Browse the repository at this point in the history
Custom tip
  • Loading branch information
igorkamyshev committed Apr 17, 2019
2 parents e69b302 + 5340633 commit 1d08105
Show file tree
Hide file tree
Showing 37 changed files with 525 additions and 9 deletions.
15 changes: 15 additions & 0 deletions back/evolutions/9.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE public.custom_tip (
"id" character varying NOT NULL,
"title" character varying NOT NULL,
"text" character varying NOT NULL,
"link" character varying,
"expireAt" timestamp with time zone NOT NULL,
"important" BOOLEAN NOT NULL DEFAULT FALSE
);

ALTER TABLE ONLY public.custom_tip
ADD CONSTRAINT "PK_custom_tip_id" PRIMARY KEY ("id");

#DOWN

DROP TABLE public.custom_tip;
25 changes: 25 additions & 0 deletions back/src/mind/application/TipsCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common'

import { EntitySaver } from '@back/db/EntitySaver'
import { CustomTipModel } from '@shared/models/mind/CustomTipModel'
import { IdGenerator } from '@back/utils/infrastructure/IdGenerator/IdGenerator'

import { DisabledTip } from '../domain/DisabledTip.entity'
import { CustomTip } from '../domain/CustomTip.entity'

@Injectable()
export class TipsCreator {
public constructor(
private readonly entitySaver: EntitySaver,
private readonly idGenerator: IdGenerator,
) {}

public async createCustom(fields: CustomTipModel): Promise<void> {
const id = await this.idGenerator.getId()
const { title, text, expireAt, important, link } = fields

const tip = new CustomTip(id, title, text, expireAt, important, link)

await this.entitySaver.save(tip)
}
}
35 changes: 35 additions & 0 deletions back/src/mind/application/adviser/CustomAdviser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as md5 from 'md5'

import { TipModel } from '@shared/models/mind/TipModel'
import { CustomTip } from '@back/mind/domain/CustomTip.entity'
import { CustomTipRepository } from '@back/mind/domain/CustomTipRepository'
import { TipAction } from '@shared/enum/TipAction'

import { Adviser } from '../../infrastructure/adviser/helpers/Adviser'
import { IsAdviser } from '../../infrastructure/adviser/helpers/IsAdviser'

@IsAdviser()
export class CustomAdviser implements Adviser {
public constructor(private readonly customTipRepo: CustomTipRepository) {}

public async giveAdvice(): Promise<TipModel[]> {
const tips = await this.customTipRepo.findActual()
const now = new Date()

return tips.map(tip => ({
action: TipAction.Custom,
meta: tip,
token: this.createToken(tip, TipAction.Custom),
date: now,
}))
}

private createToken(tip: CustomTip, action: TipAction): string {
const payload = {
id: tip.id,
action,
}

return md5(JSON.stringify(payload))
}
}
40 changes: 40 additions & 0 deletions back/src/mind/domain/CustomTip.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Column, Entity, PrimaryColumn } from 'typeorm'

import { CustomTipModel } from '@shared/models/mind/CustomTipModel'

@Entity()
export class CustomTip implements CustomTipModel {
@PrimaryColumn()
public readonly id: string

@Column()
public readonly title: string

@Column()
public readonly text: string

@Column()
public readonly link?: string

@Column()
public readonly expireAt: Date

@Column()
public readonly important: boolean

public constructor(
id: string,
title: string,
text: string,
expireAt: Date,
important: boolean = false,
link?: string,
) {
this.id = id
this.title = title
this.text = text
this.expireAt = expireAt
this.important = important
this.link = link
}
}
25 changes: 25 additions & 0 deletions back/src/mind/domain/CustomTipRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'

import { CustomTip } from './CustomTip.entity'

@Injectable()
class CustomTipRepo {
public constructor(
@InjectRepository(CustomTip)
private readonly customTipRepo: Repository<CustomTip>,
) {}

public async findActual(): Promise<CustomTip[]> {
const now = new Date().toISOString()

return this.customTipRepo
.createQueryBuilder('tip')
.where('tip.expireAt >= :now', { now })
.getMany()
}
}

export const CustomTipRepository = CustomTipRepo
export type CustomTipRepository = CustomTipRepo
8 changes: 8 additions & 0 deletions back/src/mind/mind.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,33 @@ import { TypoController } from './presentation/http/controller/TypoController'
import { TypoMerger } from './application/TypoMerger'
import { BudgetAdviser } from './application/adviser/BudgetAdviser'
import { ExtraSpendingAdviser } from './application/adviser/ExtraSpendingAdviser'
import { TipsCreator } from './application/TipsCreator'
import { CustomAdviser } from './application/adviser/CustomAdviser'
import { CustomTip } from './domain/CustomTip.entity'
import { CustomTipRepository } from './domain/CustomTipRepository'

@Module({
imports: [
UserModule,
DbModule,
MoneyModule,
TypeOrmModule.forFeature([DisabledTip]),
TypeOrmModule.forFeature([CustomTip]),
],
controllers: [TipController, TypoController],
providers: [
TypoFinder,
TypoMerger,
TypoAdviser,
BudgetAdviser,
CustomAdviser,
ExtraSpendingAdviser,
AdviserUnity,
TipsFilter,
TipsDisabler,
TipsCreator,
DisabledTipRepository,
CustomTipRepository,
],
})
export class MindModule implements NestModule {
Expand Down
12 changes: 12 additions & 0 deletions back/src/mind/presentation/http/controller/TipController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import {
ApiOkResponse,
} from '@nestjs/swagger'

import { OnlyForManager } from '@back/user/presentation/http/security/OnlyForManager'
import { OnlyForUsers } from '@back/user/presentation/http/security/OnlyForUsers'
import { TokenPayloadModel } from '@shared/models/user/TokenPayloadModel'
import { CurrentUser } from '@back/user/presentation/http/decorator/CurrentUser'
import { AdviserUnity } from '@back/mind/infrastructure/adviser/AdviserUnity'
import { TipsFilter } from '@back/mind/application/TipsFilter'
import { TipsDisabler } from '@back/mind/application/TipsDisabler'
import { TipsCreator } from '@back/mind/application/TipsCreator'

import { TipResponse } from '../reponse/TipResponse'
import { DisableTipRequest } from '../request/DisableTipRequest'
import { CustomTipRequest } from '../request/CustomTipRequest'

@Controller('mind/tip')
@OnlyForUsers()
Expand All @@ -25,6 +28,7 @@ export class TipController {
private readonly adviser: AdviserUnity,
private readonly tipsFilter: TipsFilter,
private readonly tipsDisabler: TipsDisabler,
private readonly tipsCreator: TipsCreator,
) {}

@Get()
Expand Down Expand Up @@ -54,4 +58,12 @@ export class TipController {
) {
await this.tipsDisabler.disable(request.tokens, user.login)
}

@Post('create')
@OnlyForManager()
@ApiOperation({ title: 'Create custom tip' })
@ApiOkResponse({ description: 'Created' })
public async create(@Body() request: CustomTipRequest) {
await this.tipsCreator.createCustom(request)
}
}
20 changes: 20 additions & 0 deletions back/src/mind/presentation/http/request/CustomTipRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiModelProperty } from '@nestjs/swagger'

import { CustomTipModel } from '@shared/models/mind/CustomTipModel'

export class CustomTipRequest implements CustomTipModel {
@ApiModelProperty({ example: 'Alert' })
public readonly title: string

@ApiModelProperty({ example: 'All cool' })
public readonly text: string

@ApiModelProperty({ example: 'https://google.com', required: false })
public readonly link?: string

@ApiModelProperty({ example: new Date() })
public readonly expireAt: Date

@ApiModelProperty({ example: true })
public readonly important: boolean
}
2 changes: 1 addition & 1 deletion back/src/user/presentation/http/security/OnlyForManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { UseGuards } from '@nestjs/common'

import { JwtManagerGuard } from './JwtManagerGuard'

export const OnlyForUsers = () => UseGuards(JwtManagerGuard)
export const OnlyForManager = () => UseGuards(JwtManagerGuard)
4 changes: 3 additions & 1 deletion front/pages/internal/manager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as React from 'react'

import { Manager } from '@front/features/manager'

export default class HisotryPage extends React.Component {
public static isSecure = true

public render() {
return <p>MANAGER</p>
return <Manager />
}
}
10 changes: 10 additions & 0 deletions front/src/domain/mind/actions/createTip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { fetchOrFail } from '@front/domain/store'
import { CustomTipModel } from '@shared/models/mind/CustomTipModel'

import { createTipRequest } from '../api/createTipRequest'
import { actions as tipFetchingActions } from '../reducer/createTipFetching'

export const createTip = (tip: CustomTipModel) =>
fetchOrFail(tipFetchingActions, async (_, getApi) => {
await createTipRequest(getApi())(tip)
})
7 changes: 7 additions & 0 deletions front/src/domain/mind/api/createTipRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Api } from '@front/domain/api'
import { CustomTipModel } from '@shared/models/mind/CustomTipModel'

export const createTipRequest = (api: Api) => (
tip: CustomTipModel,
): Promise<void> =>
api.client.post('/mind/tip/create', tip).then(response => response.data)
7 changes: 7 additions & 0 deletions front/src/domain/mind/reducer/createTipFetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createFetchingRedux, FetchingState } from 'redux-clear'

type State = FetchingState

const { reducer, actions } = createFetchingRedux('manager/create-tip-fetching')

export { reducer, actions, State }
6 changes: 6 additions & 0 deletions front/src/domain/mind/reducer/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { combineReducers } from 'redux'

import { reducer as tipsReducer, State as TipsState } from './tips'
import {
reducer as createTipFetchingReducer,
State as CreateTipTipState,
} from './createTipFetching'

interface State {
tips: TipsState
createTipFetching: CreateTipTipState
}

const reducer = combineReducers<State>({
tips: tipsReducer,
createTipFetching: createTipFetchingReducer,
})

export { reducer, State }
4 changes: 4 additions & 0 deletions front/src/domain/mind/selectors/getCreateTipFetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { State } from '@front/domain/store'

export const getCreateTipFetching = (state: State) =>
state.mind.createTipFetching
23 changes: 23 additions & 0 deletions front/src/features/app/features/now/tips/custom/Custom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TipModel } from '@shared/models/mind/TipModel'
import { Card } from '@front/ui/components/layout/card'

import { CustomMeta } from './CustomMeta'
import { DismissButton } from '../components/dismiss-button'
import { ExternalLink } from '@front/ui/components/controls/external-link'

interface Props {
tip: TipModel<CustomMeta>
}

export const Custom = ({ tip: { meta, token } }: Props) => {
const { title, text, link, important } = meta

const actions = !!link && <ExternalLink href={link}>Open</ExternalLink>
const dismiss = important && <DismissButton token={token} />

return (
<Card title={title} extra={dismiss} actions={[actions].filter(Boolean)}>
<p>{text}</p>
</Card>
)
}
3 changes: 3 additions & 0 deletions front/src/features/app/features/now/tips/custom/CustomMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CustomTipModel } from '@shared/models/mind/CustomTipModel'

export type CustomMeta = CustomTipModel
1 change: 1 addition & 0 deletions front/src/features/app/features/now/tips/custom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Custom } from './Custom'
2 changes: 2 additions & 0 deletions front/src/features/app/features/now/tips/getTipComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MergeSources } from './merge-sources'
import { MergeCategories } from './merge-categories'
import { DailyBudget } from './daily-budget'
import { ExtraSpending } from './extra-spending'
import { Custom } from './custom'

interface Props {
tip: TipModel
Expand All @@ -18,4 +19,5 @@ export const getTipComponent = (tip: TipModel): ComponentType<Props> =>
[TipAction.MergeCategories]: MergeCategories,
[TipAction.DailyBudget]: DailyBudget,
[TipAction.ExtraSpending]: ExtraSpending,
[TipAction.Custom]: Custom,
}[tip.action])
23 changes: 23 additions & 0 deletions front/src/features/final-form/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Field, FieldRenderProps } from 'react-final-form'
import { Diff } from 'utility-types'

import {
Checkbox as JustCheckbox,
CheckboxProps,
} from '@front/ui/components/form/checkbox'

interface OwnProps {
name: string
}

type ComponentProps = Diff<CheckboxProps, FieldRenderProps['input']>

export const Checkbox = ({
name,
...componentProps
}: OwnProps & ComponentProps) => (
<Field
name={name}
render={({ input }) => <JustCheckbox {...componentProps} {...input} />}
/>
)
Loading

0 comments on commit 1d08105

Please sign in to comment.