Merge pull request #7 from exstake/save-user-balances
Save user balances
This commit is contained in:
8
app/controllers/admin/balances_controller.rb
Normal file
8
app/controllers/admin/balances_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
module Admin
|
||||||
|
class BalancesController < Admin::ApplicationController
|
||||||
|
def valid_action?(name, resource = resource_class)
|
||||||
|
["destroy"].exclude?(name.to_s) && super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
8
app/controllers/admin/currencies_controller.rb
Normal file
8
app/controllers/admin/currencies_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
module Admin
|
||||||
|
class CurrenciesController < Admin::ApplicationController
|
||||||
|
def valid_action?(name, resource = resource_class)
|
||||||
|
["new", "edit", "destroy"].exclude?(name.to_s) && super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include Pundit
|
|
||||||
|
|
||||||
before_action :configure_devise_permitted_parameters, if: :devise_controller?
|
before_action :configure_devise_permitted_parameters, if: :devise_controller?
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class GraphqlController < ApplicationController
|
class GraphqlController < ApplicationController
|
||||||
|
# protect_from_forgery with: :null_session
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
variables = prepare_variables(params[:variables])
|
variables = prepare_variables(params[:variables])
|
||||||
query = params[:query]
|
query = params[:query]
|
||||||
|
|||||||
54
app/dashboards/balance_dashboard.rb
Normal file
54
app/dashboards/balance_dashboard.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require "administrate/base_dashboard"
|
||||||
|
|
||||||
|
class BalanceDashboard < Administrate::BaseDashboard
|
||||||
|
# ATTRIBUTE_TYPES
|
||||||
|
# a hash that describes the type of each of the model's fields.
|
||||||
|
#
|
||||||
|
# Each different type represents an Administrate::Field object,
|
||||||
|
# which determines how the attribute is displayed
|
||||||
|
# on pages throughout the dashboard.
|
||||||
|
ATTRIBUTE_TYPES = {
|
||||||
|
user: Field::BelongsTo,
|
||||||
|
currency: Field::BelongsTo,
|
||||||
|
id: Field::Number,
|
||||||
|
amount: Field::String.with_options(searchable: false),
|
||||||
|
created_at: Field::DateTime,
|
||||||
|
updated_at: Field::DateTime,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# COLLECTION_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed on the model's index page.
|
||||||
|
#
|
||||||
|
# By default, it's limited to four items to reduce clutter on index pages.
|
||||||
|
# Feel free to add, remove, or rearrange items.
|
||||||
|
COLLECTION_ATTRIBUTES = [:user, :currency, :id, :amount].freeze
|
||||||
|
|
||||||
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
|
SHOW_PAGE_ATTRIBUTES = [:user, :currency, :id, :amount, :created_at, :updated_at].freeze
|
||||||
|
|
||||||
|
# FORM_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed
|
||||||
|
# on the model's form (`new` and `edit`) pages.
|
||||||
|
FORM_ATTRIBUTES = [:user, :currency, :amount].freeze
|
||||||
|
|
||||||
|
# COLLECTION_FILTERS
|
||||||
|
# a hash that defines filters that can be used while searching via the search
|
||||||
|
# field of the dashboard.
|
||||||
|
#
|
||||||
|
# For example to add an option to search for open resources by typing "open:"
|
||||||
|
# in the search field:
|
||||||
|
#
|
||||||
|
# COLLECTION_FILTERS = {
|
||||||
|
# open: ->(resources) { resources.where(open: true) }
|
||||||
|
# }.freeze
|
||||||
|
COLLECTION_FILTERS = {}.freeze
|
||||||
|
|
||||||
|
# Overwrite this method to customize how balances are displayed
|
||||||
|
# across all pages of the admin dashboard.
|
||||||
|
#
|
||||||
|
# def display_resource(balance)
|
||||||
|
# "Balance ##{balance.id}"
|
||||||
|
# end
|
||||||
|
end
|
||||||
52
app/dashboards/currency_dashboard.rb
Normal file
52
app/dashboards/currency_dashboard.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require "administrate/base_dashboard"
|
||||||
|
|
||||||
|
class CurrencyDashboard < Administrate::BaseDashboard
|
||||||
|
# ATTRIBUTE_TYPES
|
||||||
|
# a hash that describes the type of each of the model's fields.
|
||||||
|
#
|
||||||
|
# Each different type represents an Administrate::Field object,
|
||||||
|
# which determines how the attribute is displayed
|
||||||
|
# on pages throughout the dashboard.
|
||||||
|
ATTRIBUTE_TYPES = {
|
||||||
|
id: Field::Number,
|
||||||
|
name: Field::String,
|
||||||
|
created_at: Field::DateTime,
|
||||||
|
updated_at: Field::DateTime,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# COLLECTION_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed on the model's index page.
|
||||||
|
#
|
||||||
|
# By default, it's limited to four items to reduce clutter on index pages.
|
||||||
|
# Feel free to add, remove, or rearrange items.
|
||||||
|
COLLECTION_ATTRIBUTES = [:id, :name, :created_at, :updated_at].freeze
|
||||||
|
|
||||||
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
|
SHOW_PAGE_ATTRIBUTES = [:id, :name, :created_at, :updated_at].freeze
|
||||||
|
|
||||||
|
# FORM_ATTRIBUTES
|
||||||
|
# an array of attributes that will be displayed
|
||||||
|
# on the model's form (`new` and `edit`) pages.
|
||||||
|
FORM_ATTRIBUTES = [:name].freeze
|
||||||
|
|
||||||
|
# COLLECTION_FILTERS
|
||||||
|
# a hash that defines filters that can be used while searching via the search
|
||||||
|
# field of the dashboard.
|
||||||
|
#
|
||||||
|
# For example to add an option to search for open resources by typing "open:"
|
||||||
|
# in the search field:
|
||||||
|
#
|
||||||
|
# COLLECTION_FILTERS = {
|
||||||
|
# open: ->(resources) { resources.where(open: true) }
|
||||||
|
# }.freeze
|
||||||
|
COLLECTION_FILTERS = {}.freeze
|
||||||
|
|
||||||
|
# Overwrite this method to customize how currencies are displayed
|
||||||
|
# across all pages of the admin dashboard.
|
||||||
|
#
|
||||||
|
def display_resource(currency)
|
||||||
|
currency.name
|
||||||
|
end
|
||||||
|
end
|
||||||
13
app/graphql/types/balance_type.rb
Normal file
13
app/graphql/types/balance_type.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
module Types
|
||||||
|
class BalanceType < Types::BaseObject
|
||||||
|
implements GraphQL::Types::Relay::Node
|
||||||
|
global_id_field :id
|
||||||
|
|
||||||
|
graphql_name "Balance"
|
||||||
|
|
||||||
|
field :id, ID, null: false
|
||||||
|
field :currency, CurrencyType, null: false
|
||||||
|
field :amount, String, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/graphql/types/currency_type.rb
Normal file
12
app/graphql/types/currency_type.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
module Types
|
||||||
|
class CurrencyType < Types::BaseObject
|
||||||
|
implements GraphQL::Types::Relay::Node
|
||||||
|
global_id_field :id
|
||||||
|
|
||||||
|
graphql_name "Currency"
|
||||||
|
|
||||||
|
field :id, ID, null: false
|
||||||
|
field :name, String, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,5 +8,10 @@ module Types
|
|||||||
def current_user
|
def current_user
|
||||||
context[:current_user]
|
context[:current_user]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :balances, BalanceType.connection_type, null: false
|
||||||
|
def balances
|
||||||
|
Pundit.policy_scope(current_user, Balance)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
module Types
|
module Types
|
||||||
class UserType < Types::BaseObject
|
class UserType < Types::BaseObject
|
||||||
# implements GraphQL::Types::Relay::Node
|
# implements GraphQL::Types::Relay::Node
|
||||||
|
|
||||||
global_id_field :id
|
global_id_field :id
|
||||||
|
|
||||||
|
graphql_name "User"
|
||||||
|
|
||||||
field :id, ID, null: false
|
field :id, ID, null: false
|
||||||
field :first_name, String, null: false
|
field :first_name, String, null: false
|
||||||
field :last_name, String, null: false
|
field :last_name, String, null: false
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ class XStakeSchema < GraphQL::Schema
|
|||||||
query(Types::QueryType)
|
query(Types::QueryType)
|
||||||
|
|
||||||
def self.resolve_type(abstract_type, obj, ctx)
|
def self.resolve_type(abstract_type, obj, ctx)
|
||||||
raise(GraphQL::RequiredImplementationMissingError)
|
case obj
|
||||||
|
when Currency
|
||||||
|
Types::CurrencyType
|
||||||
|
when Balance
|
||||||
|
Types::BalanceType
|
||||||
|
else
|
||||||
|
raise(GraphQL::RequiredImplementationMissingError, "Unexpected object: #{obj}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.id_from_object(object, type_definition, query_ctx)
|
def self.id_from_object(object, type_definition, ctx)
|
||||||
GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
|
GraphQL::Schema::UniqueWithinType.encode(type_definition.name, object.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.object_from_id(id, query_ctx)
|
def self.object_from_id(id, ctx)
|
||||||
type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
|
type_name, item_id = GraphQL::Schema::UniqueWithinType.decode(id)
|
||||||
type_name.constantize.find(item_id)
|
Pundit.policy_scope(ctx[:current_user], type_name.constantize).find(item_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
122
app/javascript/__generated__/schema.graphql
generated
122
app/javascript/__generated__/schema.graphql
generated
@@ -1,5 +1,127 @@
|
|||||||
|
type Balance implements Node {
|
||||||
|
amount: Float!
|
||||||
|
currency: Currency!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The connection type for Balance.
|
||||||
|
"""
|
||||||
|
type BalanceConnection {
|
||||||
|
"""
|
||||||
|
A list of edges.
|
||||||
|
"""
|
||||||
|
edges: [BalanceEdge]
|
||||||
|
|
||||||
|
"""
|
||||||
|
A list of nodes.
|
||||||
|
"""
|
||||||
|
nodes: [Balance]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Information to aid in pagination.
|
||||||
|
"""
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An edge in a connection.
|
||||||
|
"""
|
||||||
|
type BalanceEdge {
|
||||||
|
"""
|
||||||
|
A cursor for use in pagination.
|
||||||
|
"""
|
||||||
|
cursor: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The item at the end of the edge.
|
||||||
|
"""
|
||||||
|
node: Balance
|
||||||
|
}
|
||||||
|
|
||||||
|
type Currency implements Node {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An object with an ID.
|
||||||
|
"""
|
||||||
|
interface Node {
|
||||||
|
"""
|
||||||
|
ID of the object.
|
||||||
|
"""
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Information about pagination in a connection.
|
||||||
|
"""
|
||||||
|
type PageInfo {
|
||||||
|
"""
|
||||||
|
When paginating forwards, the cursor to continue.
|
||||||
|
"""
|
||||||
|
endCursor: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating forwards, are there more items?
|
||||||
|
"""
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating backwards, are there more items?
|
||||||
|
"""
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating backwards, the cursor to continue.
|
||||||
|
"""
|
||||||
|
startCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
balances(
|
||||||
|
"""
|
||||||
|
Returns the elements in the list that come after the specified cursor.
|
||||||
|
"""
|
||||||
|
after: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the elements in the list that come before the specified cursor.
|
||||||
|
"""
|
||||||
|
before: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the first _n_ elements from the list.
|
||||||
|
"""
|
||||||
|
first: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the last _n_ elements from the list.
|
||||||
|
"""
|
||||||
|
last: Int
|
||||||
|
): BalanceConnection!
|
||||||
currentUser: User
|
currentUser: User
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fetches an object given its ID.
|
||||||
|
"""
|
||||||
|
node(
|
||||||
|
"""
|
||||||
|
ID of the object.
|
||||||
|
"""
|
||||||
|
id: ID!
|
||||||
|
): Node
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fetches a list of objects given a list of IDs.
|
||||||
|
"""
|
||||||
|
nodes(
|
||||||
|
"""
|
||||||
|
IDs of the objects.
|
||||||
|
"""
|
||||||
|
ids: [ID!]!
|
||||||
|
): [Node]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import type { FC } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom";
|
||||||
|
|
||||||
import { Home } from "./pages";
|
import { Home, Wallet } from "./pages";
|
||||||
|
|
||||||
export const Routes: FC = () => {
|
export const Routes: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/">
|
<Route exact path="/">
|
||||||
<Home />
|
<Home />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/wallet">
|
||||||
|
<Wallet />
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ const MenuItems: MenuItem[] = [
|
|||||||
label: "Início",
|
label: "Início",
|
||||||
path: "/",
|
path: "/",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Stake",
|
|
||||||
path: "/stake",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Carteira",
|
label: "Carteira",
|
||||||
path: "/wallet",
|
path: "/wallet",
|
||||||
|
|||||||
94
app/javascript/src/pages/Wallet/Wallet.tsx
Normal file
94
app/javascript/src/pages/Wallet/Wallet.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { graphql } from "babel-plugin-relay/macro";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import { useLazyLoadQuery } from "react-relay";
|
||||||
|
|
||||||
|
import { tokens } from "../../constants/pancake/Tokens";
|
||||||
|
import type { WalletQuery } from "./__generated__/WalletQuery.graphql";
|
||||||
|
|
||||||
|
export const Wallet: FC = () => {
|
||||||
|
const { balances } = useLazyLoadQuery<WalletQuery>(
|
||||||
|
graphql`
|
||||||
|
query WalletQuery {
|
||||||
|
balances {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokensList = Object.values(tokens);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full w-full overflow-x-hidden mt-16">
|
||||||
|
<div className="container mx-auto px-4 sm:px-8 max-w-3xl">
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="-mx-4 sm:-mx-8 px-4 sm:px-8 py-4 overflow-x-auto">
|
||||||
|
<div className="inline-block min-w-full shadow rounded-lg overflow-hidden">
|
||||||
|
<table className="min-w-full leading-normal">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
|
||||||
|
>
|
||||||
|
Moeda
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-5 py-3 bg-white border-b border-gray-200 text-gray-800 text-left text-sm uppercase font-normal"
|
||||||
|
>
|
||||||
|
Saldo
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{balances.nodes?.map((balance) => {
|
||||||
|
const token = tokensList.find(
|
||||||
|
({ symbol }) => symbol === balance?.currency.name
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={balance?.id}>
|
||||||
|
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<a href="/" className="block relative">
|
||||||
|
<img
|
||||||
|
alt="profil"
|
||||||
|
src={`https://pancakeswap.finance/images/tokens/${token?.address["56"]}.svg`}
|
||||||
|
className="mx-auto object-cover rounded-full h-10 w-10 "
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-gray-900 whitespace-no-wrap">
|
||||||
|
{balance?.currency.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||||||
|
<p className="text-gray-900 whitespace-no-wrap">
|
||||||
|
{balance?.amount}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
165
app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts
generated
Normal file
165
app/javascript/src/pages/Wallet/__generated__/WalletQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from "relay-runtime";
|
||||||
|
export type WalletQueryVariables = {};
|
||||||
|
export type WalletQueryResponse = {
|
||||||
|
readonly balances: {
|
||||||
|
readonly nodes: ReadonlyArray<{
|
||||||
|
readonly id: string;
|
||||||
|
readonly amount: number;
|
||||||
|
readonly currency: {
|
||||||
|
readonly name: string;
|
||||||
|
};
|
||||||
|
} | null> | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type WalletQuery = {
|
||||||
|
readonly response: WalletQueryResponse;
|
||||||
|
readonly variables: WalletQueryVariables;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
query WalletQuery {
|
||||||
|
balances {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
currency {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v1 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "amount",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v2 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "name",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "WalletQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "BalanceConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "balances",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Balance",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "nodes",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Currency",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "currency",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "WalletQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "BalanceConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "balances",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Balance",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "nodes",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Currency",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "currency",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "6b8d0c664bd2d9df4d323c19c4a823a5",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "WalletQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query WalletQuery {\n balances {\n nodes {\n id\n amount\n currency {\n name\n id\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
(node as any).hash = '428f4f1ab769f9056dd38ec641a30733';
|
||||||
|
export default node;
|
||||||
1
app/javascript/src/pages/Wallet/index.ts
Normal file
1
app/javascript/src/pages/Wallet/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Wallet";
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./Home";
|
export * from "./Home";
|
||||||
|
export * from "./Wallet";
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./Home";
|
|
||||||
29
app/models/balance.rb
Normal file
29
app/models/balance.rb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: balances
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# amount :decimal(20, 10) default(0.0), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# currency_id :bigint not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_balances_on_currency_id (currency_id)
|
||||||
|
# index_balances_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (currency_id => currencies.id)
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
class Balance < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :currency
|
||||||
|
|
||||||
|
validates :amount, presence: true
|
||||||
|
end
|
||||||
14
app/models/currency.rb
Normal file
14
app/models/currency.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: currencies
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class Currency < ApplicationRecord
|
||||||
|
validates :name, presence: true
|
||||||
|
end
|
||||||
@@ -25,6 +25,7 @@ class User < ApplicationRecord
|
|||||||
:recoverable, :rememberable, :validatable
|
:recoverable, :rememberable, :validatable
|
||||||
|
|
||||||
has_many :documents, class_name: "UserDocument", dependent: :destroy
|
has_many :documents, class_name: "UserDocument", dependent: :destroy
|
||||||
|
has_many :balances, dependent: :restrict_with_error
|
||||||
|
|
||||||
validates :first_name, :last_name, :email, presence: true
|
validates :first_name, :last_name, :email, presence: true
|
||||||
validates :email, uniqueness: true
|
validates :email, uniqueness: true
|
||||||
|
|||||||
10
app/policies/balance_policy.rb
Normal file
10
app/policies/balance_policy.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class BalancePolicy < ApplicationPolicy
|
||||||
|
class Scope < Scope
|
||||||
|
def resolve
|
||||||
|
return scope.none if user.nil?
|
||||||
|
|
||||||
|
scope.where(user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,9 +5,16 @@ pt-BR:
|
|||||||
one: Documentos de Usuário
|
one: Documentos de Usuário
|
||||||
other: Documentos de Usuários
|
other: Documentos de Usuários
|
||||||
admin_user:
|
admin_user:
|
||||||
one: Usuário Administrador
|
one: Administrador
|
||||||
other: Usuários Administradores
|
other: Administradores
|
||||||
|
currency:
|
||||||
|
one: Moeda
|
||||||
|
other: Moedas
|
||||||
|
balance:
|
||||||
|
one: Saldo
|
||||||
|
other: Saldos
|
||||||
attributes:
|
attributes:
|
||||||
user:
|
user:
|
||||||
first_name: Primeiro nome
|
first_name: Primeiro nome
|
||||||
last_name: Último nome
|
last_name: Último nome
|
||||||
|
full_name: Nome completo
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :users
|
resources :users
|
||||||
resources :user_documents
|
resources :balances
|
||||||
|
resources :currencies
|
||||||
resources :admin_users
|
resources :admin_users
|
||||||
|
|
||||||
root to: "users#index"
|
root to: "users#index"
|
||||||
|
|||||||
10
db/migrate/20210811014107_create_currencies.rb
Normal file
10
db/migrate/20210811014107_create_currencies.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class CreateCurrencies < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table(:currencies) do |t|
|
||||||
|
t.string(:name, null: false)
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
db/migrate/20210811121726_create_balances.rb
Normal file
13
db/migrate/20210811121726_create_balances.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
class CreateBalances < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table(:balances) do |t|
|
||||||
|
t.references(:user, null: false, foreign_key: true)
|
||||||
|
t.references(:currency, null: false, foreign_key: true)
|
||||||
|
|
||||||
|
t.decimal(:amount, precision: 20, scale: 10, null: false, default: 0)
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
20
db/schema.rb
generated
20
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_08_08_153626) do
|
ActiveRecord::Schema.define(version: 2021_08_11_121726) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -55,6 +55,22 @@ ActiveRecord::Schema.define(version: 2021_08_08_153626) do
|
|||||||
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "balances", force: :cascade do |t|
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "currency_id", null: false
|
||||||
|
t.decimal "amount", precision: 20, scale: 10, default: "0.0", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["currency_id"], name: "index_balances_on_currency_id"
|
||||||
|
t.index ["user_id"], name: "index_balances_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "currencies", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "user_documents", force: :cascade do |t|
|
create_table "user_documents", force: :cascade do |t|
|
||||||
t.string "status", null: false
|
t.string "status", null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
@@ -79,5 +95,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_153626) do
|
|||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
|
add_foreign_key "balances", "currencies"
|
||||||
|
add_foreign_key "balances", "users"
|
||||||
add_foreign_key "user_documents", "users"
|
add_foreign_key "user_documents", "users"
|
||||||
end
|
end
|
||||||
|
|||||||
222
db/seeds.rb
222
db/seeds.rb
@@ -1,8 +1,216 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
# This file should contain all the record creation needed to seed the database with its default values.
|
|
||||||
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
|
AdminUser.create(email: "admin@example.com", password: "password")
|
||||||
#
|
|
||||||
# Examples:
|
user = User.create!(
|
||||||
#
|
first_name: "Test",
|
||||||
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
|
last_name: "User",
|
||||||
# Character.create(name: 'Luke', movie: movies.first)
|
email: "user@example.com",
|
||||||
|
password: "password"
|
||||||
|
)
|
||||||
|
|
||||||
|
tokens = [
|
||||||
|
"BNB",
|
||||||
|
"CAKE",
|
||||||
|
"CHESS",
|
||||||
|
"TITAN",
|
||||||
|
"ONE",
|
||||||
|
"MASK",
|
||||||
|
"DVI",
|
||||||
|
"ADX",
|
||||||
|
"BSCPAD",
|
||||||
|
"RABBIT",
|
||||||
|
"FORM",
|
||||||
|
"TXL",
|
||||||
|
"ORBS",
|
||||||
|
"COS",
|
||||||
|
"BUNNY",
|
||||||
|
"ALICE",
|
||||||
|
"FOR",
|
||||||
|
"BUX",
|
||||||
|
"NULS",
|
||||||
|
"BELT",
|
||||||
|
"RAMP",
|
||||||
|
"BFI",
|
||||||
|
"DEXE",
|
||||||
|
"BEL",
|
||||||
|
"TPT",
|
||||||
|
"WATCH",
|
||||||
|
"xMARK",
|
||||||
|
"bMXX",
|
||||||
|
"IOTX",
|
||||||
|
"BOR",
|
||||||
|
"bOPEN",
|
||||||
|
"DODO",
|
||||||
|
"SWINGBY",
|
||||||
|
"BRY",
|
||||||
|
"ZEE",
|
||||||
|
"SWGb",
|
||||||
|
"SWG",
|
||||||
|
"SFP",
|
||||||
|
"LINA",
|
||||||
|
"LIT",
|
||||||
|
"HGET",
|
||||||
|
"BDO",
|
||||||
|
"EGLD",
|
||||||
|
"UST",
|
||||||
|
"wSOTE",
|
||||||
|
"FRONT",
|
||||||
|
"Helmet",
|
||||||
|
"BTCST",
|
||||||
|
"BSCX",
|
||||||
|
"TEN",
|
||||||
|
"bALBT",
|
||||||
|
"ASR",
|
||||||
|
"ATM",
|
||||||
|
"OG",
|
||||||
|
"REEF",
|
||||||
|
"DITTO",
|
||||||
|
"JUV",
|
||||||
|
"PSG",
|
||||||
|
"VAI",
|
||||||
|
"wBNB",
|
||||||
|
"BLINK",
|
||||||
|
"UNFI",
|
||||||
|
"TWT",
|
||||||
|
"HARD",
|
||||||
|
"bROOBEE",
|
||||||
|
"STAX",
|
||||||
|
"NAR",
|
||||||
|
"NYA",
|
||||||
|
"CTK",
|
||||||
|
"INJ",
|
||||||
|
"SXP",
|
||||||
|
"ALPHA",
|
||||||
|
"XVS",
|
||||||
|
"SUSHI",
|
||||||
|
"COMP",
|
||||||
|
"SYRUP",
|
||||||
|
"BIFI",
|
||||||
|
"DUSK",
|
||||||
|
"BUSD",
|
||||||
|
"ETH",
|
||||||
|
"BETH",
|
||||||
|
"mAMZN",
|
||||||
|
"mGOOGL",
|
||||||
|
"mNFLX",
|
||||||
|
"mTSLA",
|
||||||
|
"LTC",
|
||||||
|
"USDC",
|
||||||
|
"DAI",
|
||||||
|
"ADA",
|
||||||
|
"BAND",
|
||||||
|
"DOT",
|
||||||
|
"EOS",
|
||||||
|
"LINK",
|
||||||
|
"USDT",
|
||||||
|
"BTCB",
|
||||||
|
"XRP",
|
||||||
|
"ATOM",
|
||||||
|
"YFII",
|
||||||
|
"XTZ",
|
||||||
|
"BCH",
|
||||||
|
"YFI",
|
||||||
|
"UNI",
|
||||||
|
"FIL",
|
||||||
|
"BAKE",
|
||||||
|
"BURGER",
|
||||||
|
"bDIGG",
|
||||||
|
"bBadger",
|
||||||
|
"TRADE",
|
||||||
|
"PNT",
|
||||||
|
"MIR",
|
||||||
|
"pBTC",
|
||||||
|
"LTO",
|
||||||
|
"pCWS",
|
||||||
|
"ZIL",
|
||||||
|
"LIEN",
|
||||||
|
"SWTH",
|
||||||
|
"DFT",
|
||||||
|
"GUM",
|
||||||
|
"DEGO",
|
||||||
|
"NRV",
|
||||||
|
"EASY",
|
||||||
|
"ODDZ",
|
||||||
|
"HOO",
|
||||||
|
"APYS",
|
||||||
|
"BONDLY",
|
||||||
|
"TKO",
|
||||||
|
"ITAM",
|
||||||
|
"ARPA",
|
||||||
|
"EPS",
|
||||||
|
"JGN",
|
||||||
|
"TLM",
|
||||||
|
"PERL",
|
||||||
|
"ALPA",
|
||||||
|
"HZN",
|
||||||
|
"SUTER",
|
||||||
|
"CGG",
|
||||||
|
"MIX",
|
||||||
|
"HAKKA",
|
||||||
|
"XED",
|
||||||
|
"τBTC",
|
||||||
|
"ALPACA",
|
||||||
|
"DFD",
|
||||||
|
"LMT",
|
||||||
|
"BTT",
|
||||||
|
"TRX",
|
||||||
|
"WIN",
|
||||||
|
"mCOIN",
|
||||||
|
"MATH",
|
||||||
|
"KUN",
|
||||||
|
"QSD",
|
||||||
|
"HYFI",
|
||||||
|
"OIN",
|
||||||
|
"DOGE",
|
||||||
|
"FINE",
|
||||||
|
"ONE",
|
||||||
|
"PMON",
|
||||||
|
"HOTCROSS",
|
||||||
|
"τDOGE",
|
||||||
|
"BTR",
|
||||||
|
"UBXT",
|
||||||
|
"WMASS",
|
||||||
|
"RFOX",
|
||||||
|
"XEND",
|
||||||
|
"CYC",
|
||||||
|
"CHR",
|
||||||
|
"KALM",
|
||||||
|
"DERI",
|
||||||
|
"WELL",
|
||||||
|
"WEX",
|
||||||
|
"WAULTx",
|
||||||
|
"pOPEN",
|
||||||
|
"EZ",
|
||||||
|
"VRT",
|
||||||
|
"TUSD",
|
||||||
|
"MTRG",
|
||||||
|
"KTN",
|
||||||
|
"QKC",
|
||||||
|
"bCFX",
|
||||||
|
"MX",
|
||||||
|
"ATA",
|
||||||
|
"MBOX",
|
||||||
|
"BORING",
|
||||||
|
"MARSH",
|
||||||
|
"AMPL",
|
||||||
|
"O3",
|
||||||
|
"HAI",
|
||||||
|
"HTB",
|
||||||
|
"WOO",
|
||||||
|
"$DG",
|
||||||
|
]
|
||||||
|
|
||||||
|
currencies = tokens.map do |token|
|
||||||
|
Currency.create!(name: token)
|
||||||
|
end
|
||||||
|
|
||||||
|
currencies.each do |currency|
|
||||||
|
random_floating_number = (rand * (10000 - 0) + 0)
|
||||||
|
|
||||||
|
Balance.create!(
|
||||||
|
user_id: user.id,
|
||||||
|
currency_id: currency.id,
|
||||||
|
amount: random_floating_number
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|||||||
35
spec/models/balance_spec.rb
Normal file
35
spec/models/balance_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: balances
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# amount :decimal(20, 10) default(0.0), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# currency_id :bigint not null
|
||||||
|
# user_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_balances_on_currency_id (currency_id)
|
||||||
|
# index_balances_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (currency_id => currencies.id)
|
||||||
|
# fk_rails_... (user_id => users.id)
|
||||||
|
#
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe(Balance, type: :model) do
|
||||||
|
describe "validations" do
|
||||||
|
it { is_expected.to(validate_presence_of(:amount)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "associations" do
|
||||||
|
it { is_expected.to(belong_to(:user)) }
|
||||||
|
it { is_expected.to(belong_to(:currency)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
18
spec/models/currency_spec.rb
Normal file
18
spec/models/currency_spec.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: currencies
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# name :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe(Currency, type: :model) do
|
||||||
|
describe "validations" do
|
||||||
|
it { is_expected.to(validate_presence_of(:name)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,5 +31,6 @@ RSpec.describe(User, type: :model) do
|
|||||||
|
|
||||||
describe "associations" do
|
describe "associations" do
|
||||||
it { is_expected.to(have_many(:documents)) }
|
it { is_expected.to(have_many(:documents)) }
|
||||||
|
it { is_expected.to(have_many(:balances)) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
6
spec/policies/balance_policy_spec.rb
Normal file
6
spec/policies/balance_policy_spec.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe(BalancePolicy, type: :policy) do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user