Improved styling using dark theme and improved other functionality.
This commit is contained in:
parent
6183ccbea9
commit
9b8a450234
|
@ -0,0 +1,21 @@
|
||||||
|
# Color Scheme!
|
||||||
|
|
||||||
|
`#f79256`
|
||||||
|
|
||||||
|
![](https://dummyimage.com/200x200/f79156/fff.jpg)
|
||||||
|
|
||||||
|
`#fbd1a2`
|
||||||
|
|
||||||
|
![](https://dummyimage.com/200x200/fbd2a2/fff.jpg)
|
||||||
|
|
||||||
|
`#7dcfb6`
|
||||||
|
|
||||||
|
![](https://dummyimage.com/200x200/7dcfb6/fff.jpg)
|
||||||
|
|
||||||
|
`#1d4e89`
|
||||||
|
|
||||||
|
![](https://dummyimage.com/200x200/1d4e89/fff.jpg)
|
||||||
|
|
||||||
|
`#00b2ca`
|
||||||
|
|
||||||
|
![](https://dummyimage.com/200x200/00b2ca/fff.jpg)
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 8.4666665 8.4666669"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||||
|
sodipodi:docname="icon.svg"
|
||||||
|
inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CoyoteCredit\icon_256.png"
|
||||||
|
inkscape:export-xdpi="768.00006"
|
||||||
|
inkscape:export-ydpi="768.00006"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#ffffff"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:pageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:zoom="16"
|
||||||
|
inkscape:cx="20.59375"
|
||||||
|
inkscape:cy="18.21875"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1017"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:#e88951;fill-opacity:1;stroke:#e88951;stroke-width:0.757717;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 4.4233884,2.6232631 6.376365,0.64987167 6.0568968,3.5177737 Z"
|
||||||
|
id="path2086"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<circle
|
||||||
|
style="fill:#fbd1a2;fill-opacity:1;stroke-width:1.5339;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path846"
|
||||||
|
cx="4.2333336"
|
||||||
|
cy="5.0400858"
|
||||||
|
r="3.1597018" />
|
||||||
|
<path
|
||||||
|
style="fill:#f79256;fill-opacity:1;stroke:#f79256;stroke-width:0.87652;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 2.4378179,2.9878512 4.6969909,0.70506246 4.3274351,4.0226073 Z"
|
||||||
|
id="path903"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,50 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
import javax.servlet.RequestDispatcher;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@ControllerAdvice
|
||||||
|
@RequestMapping(path = "/error")
|
||||||
|
public class ErrorPage implements ErrorController {
|
||||||
|
@GetMapping
|
||||||
|
public String get(Model model, HttpServletRequest request) {
|
||||||
|
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||||
|
int statusCode = status == null ? -1 : (Integer) status;
|
||||||
|
model.addAttribute("statusCode", statusCode);
|
||||||
|
Object message = request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
|
||||||
|
String msg;
|
||||||
|
if (message != null) {
|
||||||
|
msg = (String) message;
|
||||||
|
} else {
|
||||||
|
msg = switch (statusCode) {
|
||||||
|
case 404 -> "Not found.";
|
||||||
|
case 400 -> "Bad request.";
|
||||||
|
case 401 -> "Unauthorized.";
|
||||||
|
case 403 -> "Forbidden.";
|
||||||
|
case 500 -> "Internal server error.";
|
||||||
|
default -> "Unknown error.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
model.addAttribute("message", msg);
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
|
public ModelAndView handleException(ResponseStatusException e) {
|
||||||
|
ModelAndView mav = new ModelAndView("error");
|
||||||
|
mav.addObject("statusCode", e.getRawStatusCode());
|
||||||
|
mav.addObject("message", e.getReason());
|
||||||
|
return mav;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import nl.andrewl.coyotecredit.service.TradeableService;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/tradeables/{tradeableId}")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TradeablePage {
|
||||||
|
private final TradeableService tradeableService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String get(Model model, @PathVariable long tradeableId, @AuthenticationPrincipal User user) {
|
||||||
|
model.addAttribute("tradeable", tradeableService.getTradeable(tradeableId, user));
|
||||||
|
return "tradeable";
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,8 @@ public record FullAccountData (
|
||||||
String number,
|
String number,
|
||||||
String name,
|
String name,
|
||||||
boolean admin,
|
boolean admin,
|
||||||
boolean userAdmin,
|
boolean userAdmin,// If the current user is an admin of the exchange this account is in.
|
||||||
|
boolean userIsOwner,// If the current user is the owner of this account.
|
||||||
ExchangeData exchange,
|
ExchangeData exchange,
|
||||||
List<BalanceData> balances,
|
List<BalanceData> balances,
|
||||||
String totalBalance,
|
String totalBalance,
|
||||||
|
|
|
@ -10,6 +10,8 @@ public record FullExchangeData (
|
||||||
String name,
|
String name,
|
||||||
TradeableData primaryTradeable,
|
TradeableData primaryTradeable,
|
||||||
List<TradeableData> supportedTradeables,
|
List<TradeableData> supportedTradeables,
|
||||||
|
String totalMarketValue,
|
||||||
|
int accountCount,
|
||||||
// Account info that's needed for determining if it's possible to do some actions.
|
// Account info that's needed for determining if it's possible to do some actions.
|
||||||
boolean accountAdmin,
|
boolean accountAdmin,
|
||||||
long accountId
|
long accountId
|
||||||
|
|
|
@ -11,7 +11,10 @@ public record TradeableData(
|
||||||
String marketPriceUsd,
|
String marketPriceUsd,
|
||||||
String formattedPriceUsd,
|
String formattedPriceUsd,
|
||||||
String name,
|
String name,
|
||||||
String description
|
String description,
|
||||||
|
|
||||||
|
Long exchangeId,
|
||||||
|
String exchangeName
|
||||||
) {
|
) {
|
||||||
public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00");
|
public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00");
|
||||||
|
|
||||||
|
@ -23,7 +26,9 @@ public record TradeableData(
|
||||||
t.getMarketPriceUsd().toPlainString(),
|
t.getMarketPriceUsd().toPlainString(),
|
||||||
DECIMAL_FORMAT.format(t.getMarketPriceUsd()),
|
DECIMAL_FORMAT.format(t.getMarketPriceUsd()),
|
||||||
t.getName(),
|
t.getName(),
|
||||||
t.getDescription()
|
t.getDescription(),
|
||||||
|
t.getExchange() == null ? null : t.getExchange().getId(),
|
||||||
|
t.getExchange() == null ? null : t.getExchange().getName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ public record TransactionData(
|
||||||
t.getFromAmount().toPlainString(),
|
t.getFromAmount().toPlainString(),
|
||||||
new TradeableData(t.getTo()),
|
new TradeableData(t.getTo()),
|
||||||
t.getToAmount().toPlainString(),
|
t.getToAmount().toPlainString(),
|
||||||
t.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
t.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " UTC"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,7 @@ public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||||
List<Account> findAllByUser(User user);
|
List<Account> findAllByUser(User user);
|
||||||
Optional<Account> findByNumber(String number);
|
Optional<Account> findByNumber(String number);
|
||||||
Optional<Account> findByUserAndExchange(User user, Exchange exchange);
|
Optional<Account> findByUserAndExchange(User user, Exchange exchange);
|
||||||
|
boolean existsByUserAndExchange(User user, Exchange exchange);
|
||||||
|
|
||||||
|
List<Account> findAllByExchange(Exchange e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,11 +85,23 @@ public class Account {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total account balance, in terms of the exchange's primary asset.
|
||||||
|
* @return The total balance in terms of the exchange's primary asset.
|
||||||
|
*/
|
||||||
public BigDecimal getTotalBalance() {
|
public BigDecimal getTotalBalance() {
|
||||||
BigDecimal totalUsd = new BigDecimal(0);
|
return getTotalBalanceUsd().divide(getExchange().getPrimaryTradeable().getMarketPriceUsd(), RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the total account balance, in USD.
|
||||||
|
* @return The total balance, in USD.
|
||||||
|
*/
|
||||||
|
public BigDecimal getTotalBalanceUsd() {
|
||||||
|
BigDecimal totalUsd = BigDecimal.ZERO;
|
||||||
for (var bal : getBalances()) {
|
for (var bal : getBalances()) {
|
||||||
totalUsd = totalUsd.add(bal.getTradeable().getMarketPriceUsd().multiply(bal.getAmount()));
|
totalUsd = totalUsd.add(bal.getTradeable().getMarketPriceUsd().multiply(bal.getAmount()));
|
||||||
}
|
}
|
||||||
return totalUsd.divide(getExchange().getPrimaryTradeable().getMarketPriceUsd(), RoundingMode.HALF_UP);
|
return totalUsd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package nl.andrewl.coyotecredit.model;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a snapshot in time of an account's total value.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class AccountValueSnapshot {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private Account account;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 24, scale = 10)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
|
||||||
|
public AccountValueSnapshot(Account account, LocalDateTime timestamp, BigDecimal amount) {
|
||||||
|
this.account = account;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import nl.andrewl.coyotecredit.dao.TradeableRepository;
|
||||||
import nl.andrewl.coyotecredit.dao.TransactionRepository;
|
import nl.andrewl.coyotecredit.dao.TransactionRepository;
|
||||||
import nl.andrewl.coyotecredit.dao.TransferRepository;
|
import nl.andrewl.coyotecredit.dao.TransferRepository;
|
||||||
import nl.andrewl.coyotecredit.model.*;
|
import nl.andrewl.coyotecredit.model.*;
|
||||||
|
import nl.andrewl.coyotecredit.util.AccountNumberUtils;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
@ -52,7 +53,14 @@ public class AccountService {
|
||||||
if (!sender.getUser().getId().equals(user.getId())) {
|
if (!sender.getUser().getId().equals(user.getId())) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
Account recipient = accountRepository.findByNumber(payload.recipientNumber())
|
String recipientNumber = payload.recipientNumber().trim();
|
||||||
|
if (!AccountNumberUtils.isValid(recipientNumber)) {
|
||||||
|
throw new ResponseStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"The recipient number \"" + recipientNumber + "\" is not valid. Should be of the form 1234-1234-1234-1234."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Account recipient = accountRepository.findByNumber(recipientNumber)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown recipient."));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown recipient."));
|
||||||
Tradeable tradeable = tradeableRepository.findById(payload.tradeableId())
|
Tradeable tradeable = tradeableRepository.findById(payload.tradeableId())
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable asset."));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable asset."));
|
||||||
|
@ -115,6 +123,7 @@ public class AccountService {
|
||||||
account.getName(),
|
account.getName(),
|
||||||
account.isAdmin(),
|
account.isAdmin(),
|
||||||
userAccount.isAdmin(),
|
userAccount.isAdmin(),
|
||||||
|
account.getUser().getId().equals(user.getId()),
|
||||||
new ExchangeData(
|
new ExchangeData(
|
||||||
account.getExchange().getId(),
|
account.getExchange().getId(),
|
||||||
account.getExchange().getName(),
|
account.getExchange().getName(),
|
||||||
|
|
|
@ -35,8 +35,14 @@ public class ExchangeService {
|
||||||
public FullExchangeData getData(long exchangeId, User user) {
|
public FullExchangeData getData(long exchangeId, User user) {
|
||||||
Exchange e = exchangeRepository.findById(exchangeId)
|
Exchange e = exchangeRepository.findById(exchangeId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
Account account = accountRepository.findByUserAndExchange(user, e)
|
Account userAccount = accountRepository.findByUserAndExchange(user, e)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
BigDecimal totalValue = BigDecimal.ZERO;
|
||||||
|
int accountCount = 0;
|
||||||
|
for (var acc : accountRepository.findAllByExchange(e)) {
|
||||||
|
totalValue = totalValue.add(acc.getTotalBalance());
|
||||||
|
accountCount++;
|
||||||
|
}
|
||||||
return new FullExchangeData(
|
return new FullExchangeData(
|
||||||
e.getId(),
|
e.getId(),
|
||||||
e.getName(),
|
e.getName(),
|
||||||
|
@ -45,8 +51,10 @@ public class ExchangeService {
|
||||||
.map(TradeableData::new)
|
.map(TradeableData::new)
|
||||||
.sorted(Comparator.comparing(TradeableData::symbol))
|
.sorted(Comparator.comparing(TradeableData::symbol))
|
||||||
.toList(),
|
.toList(),
|
||||||
account.isAdmin(),
|
TradeableData.DECIMAL_FORMAT.format(totalValue),
|
||||||
account.getId()
|
accountCount,
|
||||||
|
userAccount.isAdmin(),
|
||||||
|
userAccount.getId()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||||
|
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
||||||
|
import nl.andrewl.coyotecredit.dao.TradeableRepository;
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TradeableService {
|
||||||
|
private final TradeableRepository tradeableRepository;
|
||||||
|
private final AccountRepository accountRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public TradeableData getTradeable(long id, User user) {
|
||||||
|
Tradeable tradeable = tradeableRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (tradeable.getExchange() != null && !accountRepository.existsByUserAndExchange(user, tradeable.getExchange())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return new TradeableData(tradeable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -40,7 +41,16 @@ public class TradeableUpdateService {
|
||||||
private String polygonApiKey;
|
private String polygonApiKey;
|
||||||
private static final int POLYGON_API_TIMEOUT = 15;
|
private static final int POLYGON_API_TIMEOUT = 15;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void startupUpdate() {
|
||||||
|
updatePublicTradeables();
|
||||||
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "@midnight")
|
@Scheduled(cron = "@midnight")
|
||||||
|
public void scheduledUpdate() {
|
||||||
|
updatePublicTradeables();
|
||||||
|
}
|
||||||
|
|
||||||
public void updatePublicTradeables() {
|
public void updatePublicTradeables() {
|
||||||
List<Tradeable> publicTradeables = tradeableRepository.findAllByExchangeNull();
|
List<Tradeable> publicTradeables = tradeableRepository.findAllByExchangeNull();
|
||||||
long delay = 5;
|
long delay = 5;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
server.error.whitelabel.enabled=false
|
|
@ -26,6 +26,34 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency {
|
:root {
|
||||||
|
--color-dark-peach: #f79256;
|
||||||
|
--color-light-peach: #fbd1a2;
|
||||||
|
--color-mint: #7dffb6;
|
||||||
|
--color-faded-blue: #1d4e89;
|
||||||
|
--color-light-blue: #00b2ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
font-family: SpaceMono, monospace;
|
font-family: SpaceMono, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
--bs-bg-opacity: 1;
|
||||||
|
background-color: var(--color-faded-blue);
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-light-peach);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-link:hover {
|
||||||
|
color: var(--color-mint);
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
File diff suppressed because one or more lines are too long
|
@ -6,13 +6,35 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1>Account <span th:text="${account.number()}"></span></h1>
|
<h1 class="display-4">Account <span th:text="${account.number()}"></span></h1>
|
||||||
<p>In <a th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a></p>
|
|
||||||
<p>Total value of <span class="currency" th:text="${account.totalBalance()}"></span> <span th:text="${account.exchange().primaryTradeable()}"></span></p>
|
|
||||||
|
|
||||||
<h3>Overview</h3>
|
<div class="card text-white bg-dark mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Overview</h5>
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Number</dt>
|
||||||
|
<dd class="col-sm-6 monospace" th:text="${account.number()}"></dd>
|
||||||
|
<dt class="col-sm-6">Account Holder Name</dt>
|
||||||
|
<dd class="col-sm-6" th:text="${account.name()}"></dd>
|
||||||
|
<dt class="col-sm-6">Exchange</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
<a class="colored-link" th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-6">Total Value</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
<span class="monospace" th:text="${account.totalBalance()}"></span> <span th:text="${account.exchange().primaryTradeable()}"></span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
|
||||||
|
<a class="btn btn-success" th:href="@{/accounts/{aId}/transfer(aId=${account.id()})}">Transfer</a>
|
||||||
|
<a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table">
|
<div class="card text-white bg-dark mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Tradeable Assets</h5>
|
||||||
|
<table class="table table-dark">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Asset</th>
|
<th>Asset</th>
|
||||||
|
@ -22,20 +44,21 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="bal : ${account.balances()}">
|
<tr th:each="bal : ${account.balances()}">
|
||||||
<td th:text="${bal.symbol()}"></td>
|
<td>
|
||||||
|
<a class="colored-link" th:href="@{/tradeables/{tId}(tId=${bal.id()})}" th:text="${bal.symbol()}"></a>
|
||||||
|
</td>
|
||||||
<td th:text="${bal.type()}"></td>
|
<td th:text="${bal.type()}"></td>
|
||||||
<td class="currency" th:text="${bal.amount()}"></td>
|
<td class="monospace" th:text="${bal.amount()}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
|
<div class="card text-white bg-dark mb-3" th:if="${!account.recentTransactions().isEmpty()}">
|
||||||
<a class="btn btn-success" th:href="@{/accounts/{aId}/transfer(aId=${account.id()})}">Transfer</a>
|
<div class="card-body">
|
||||||
<a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
|
<h5 class="card-title">Recent Transactions</h5>
|
||||||
|
<table class="table table-dark">
|
||||||
<div th:if="${!account.recentTransactions().isEmpty()}">
|
|
||||||
<h3>Recent Transactions</h3>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>From</th>
|
<th>From</th>
|
||||||
|
@ -47,13 +70,14 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="tx : ${account.recentTransactions()}">
|
<tr th:each="tx : ${account.recentTransactions()}">
|
||||||
<td th:text="${tx.from().name()}"></td>
|
<td th:text="${tx.from().symbol()}"></td>
|
||||||
<td class="currency" th:text="${tx.fromAmount()}"></td>
|
<td class="monospace" th:text="${tx.fromAmount()}"></td>
|
||||||
<td th:text="${tx.to().name()}"></td>
|
<td th:text="${tx.to().symbol()}"></td>
|
||||||
<td class="currency" th:text="${tx.toAmount()}"></td>
|
<td class="monospace" th:text="${tx.toAmount()}"></td>
|
||||||
<td th:text="${tx.timestamp()}"></td>
|
<td th:text="${tx.timestamp()}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
|
@ -5,6 +5,10 @@
|
||||||
th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
|
th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Transfer</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Transfer funds to other accounts.
|
||||||
|
</p>
|
||||||
<form th:action="@{/accounts/{aId}/transfer(aId=${accountId})}" th:method="post">
|
<form th:action="@{/accounts/{aId}/transfer(aId=${accountId})}" th:method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="recipientNumberInput" class="form-label">Recipient Account Number</label>
|
<label for="recipientNumberInput" class="form-label">Recipient Account Number</label>
|
||||||
|
@ -26,8 +30,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="messageTextArea" class="form-label">Message</label>
|
<label for="messageTextArea" class="form-label">Message</label>
|
||||||
<textarea class="form-control" name="message" rows="3" id="messageTextArea"></textarea>
|
<small class="text-muted">Note: Message text is readable by administrators.</small>
|
||||||
|
<textarea class="form-control" name="message" rows="3" id="messageTextArea" maxlength="1024"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Submit</button>
|
<button type="submit" class="btn btn-primary btn-lg">Transfer</button>
|
||||||
|
|
||||||
|
<p class="text-danger fw-bold">
|
||||||
|
Warning! All transfers are final, and cannot be reversed.
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Home', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="text-center">
|
||||||
|
<span class="display-4">Error</span>
|
||||||
|
<small class="text-muted" th:text="${statusCode}"></small>
|
||||||
|
</h1>
|
||||||
|
<p class="lead" th:text="${message}"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -7,13 +7,14 @@
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1>Accounts</h1>
|
<h1>Accounts</h1>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table table-dark">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Number</th>
|
<th>Number</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Admin</th>
|
<th>Admin</th>
|
||||||
<th>Balance</th>
|
<th>Balance</th>
|
||||||
|
<th>Remove</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -21,7 +22,7 @@
|
||||||
<td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
|
<td><a th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
|
||||||
<td th:text="${account.name()}"></td>
|
<td th:text="${account.name()}"></td>
|
||||||
<td th:text="${account.admin()}"></td>
|
<td th:text="${account.admin()}"></td>
|
||||||
<td class="currency" th:text="${account.totalBalance()}"></td>
|
<td class="monospace" th:text="${account.totalBalance()}"></td>
|
||||||
<td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
|
<td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -5,14 +5,28 @@
|
||||||
th:replace="~{layout/basic_page :: layout (title='Exchange', content=~{::#content})}"
|
th:replace="~{layout/basic_page :: layout (title='Exchange', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1 th:text="${exchange.name()}"></h1>
|
<h1 class="display-4" th:text="${exchange.name()}"></h1>
|
||||||
|
|
||||||
<p>
|
<div class="card text-white bg-dark mb-3">
|
||||||
Primary asset: <span th:text="${exchange.primaryTradeable().name()}"></span>
|
<div class="card-body">
|
||||||
</p>
|
<h5 class="card-title">Overview</h5>
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-6">Primary Currency</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
<a class="colored-link" th:href="@{/tradeables/{tId}(tId=${exchange.primaryTradeable().id()})}" th:text="${exchange.primaryTradeable().name()}"></a>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-6">Total Market Value</dt>
|
||||||
|
<dd class="col-sm-6">
|
||||||
|
<span class="monospace" th:text="${exchange.totalMarketValue()}"></span> <span th:text="${exchange.primaryTradeable().symbol()}"></span>
|
||||||
|
</dd>
|
||||||
|
<dt class="col-sm-6">Number of Accounts</dt>
|
||||||
|
<dd class="col-sm-6" th:text="${exchange.accountCount()}"></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Tradeable Assets</h3>
|
<h3>Tradeable Assets</h3>
|
||||||
<table class="table">
|
<table class="table table-dark">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Symbol</th>
|
<th>Symbol</th>
|
||||||
|
@ -25,7 +39,7 @@
|
||||||
<tr th:each="tradeable : ${exchange.supportedTradeables()}">
|
<tr th:each="tradeable : ${exchange.supportedTradeables()}">
|
||||||
<td th:text="${tradeable.symbol()}"></td>
|
<td th:text="${tradeable.symbol()}"></td>
|
||||||
<td th:text="${tradeable.type()}"></td>
|
<td th:text="${tradeable.type()}"></td>
|
||||||
<td class="currency" th:text="${tradeable.formattedPriceUsd()}"></td>
|
<td class="monospace" th:text="${tradeable.formattedPriceUsd()}"></td>
|
||||||
<td th:text="${tradeable.name()}"></td>
|
<td th:text="${tradeable.name()}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
th:replace="~{layout/basic_page :: layout (title='Exchanges', content=~{::#content})}"
|
th:replace="~{layout/basic_page :: layout (title='Exchanges', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1>Exchanges</h1>
|
<h1 class="display-4">Exchanges</h1>
|
||||||
<table class="table">
|
<table class="table table-dark">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
@ -18,13 +18,13 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="ed : ${exchangeData}">
|
<tr th:each="ed : ${exchangeData}">
|
||||||
<td>
|
<td>
|
||||||
<a th:text="${ed.exchange().name()}" th:href="@{/exchanges/{eId}(eId=${ed.exchange().id()})}"></a>
|
<a class="colored-link" th:text="${ed.exchange().name()}" th:href="@{/exchanges/{eId}(eId=${ed.exchange().id()})}"></a>
|
||||||
</td>
|
</td>
|
||||||
<td th:text="${ed.exchange().primaryTradeable()}"></td>
|
<td th:text="${ed.exchange().primaryTradeable()}"></td>
|
||||||
<td>
|
<td>
|
||||||
<a th:text="${ed.account().number()}" th:href="@{/accounts/{aId}(aId=${ed.account().id()})}"></a>
|
<a class="colored-link" th:text="${ed.account().number()}" th:href="@{/accounts/{aId}(aId=${ed.account().id()})}"></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="currency" th:text="${ed.account().totalBalance()}"></td>
|
<td class="monospace" th:text="${ed.account().totalBalance()}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
xmlns:th="http://www.thymeleaf.org"
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
|
|
||||||
>
|
>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
@ -10,9 +9,12 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav th:fragment="header" class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav th:fragment="header" class="navbar navbar-expand-lg navbar-dark header-bar">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="/">Coyote Credit</a>
|
<a class="navbar-brand" href="/">
|
||||||
|
<img src="/static/images/icon_256.png" alt="Coyote Credit" height="24" class="d-inline-block align-text-top"/>
|
||||||
|
Coyote Credit
|
||||||
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
||||||
<button class="btn btn-outline-success" type="submit">Logout</button>
|
<button class="btn btn-dark" type="submit">Logout</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,13 +11,16 @@
|
||||||
<h1 class="display-4">Welcome to Coyote Credit</h1>
|
<h1 class="display-4">Welcome to Coyote Credit</h1>
|
||||||
|
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
A simulated asset trading platform developed for building a stronger understanding of investment and wealth management.
|
A simulated asset trading platform developed for building a stronger understanding
|
||||||
|
of investment and wealth management.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You can visit the <a th:href="@{/exchanges}">Exchanges</a> page to view a list of exchanges that you're participating in.
|
You can visit the <a class="colored-link" th:href="@{/exchanges}">Exchanges</a> page
|
||||||
|
to view a list of exchanges that you're participating in. Within an exchange, you
|
||||||
|
may buy and sell tradeable assets, and transfer funds to other accounts.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,14 +9,10 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title th:text="${'Coyote Credit - ' + title}">Coyote Credit</title>
|
<title th:text="${'Coyote Credit - ' + title}">Coyote Credit</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/vendor/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="/static/css/style.css"/>
|
<link rel="stylesheet" href="/static/css/style.css"/>
|
||||||
<link
|
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
|
||||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico"/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -30,10 +26,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script
|
<script src="/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
|
||||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -5,13 +5,16 @@
|
||||||
th:replace="~{layout/basic_page :: layout (title='Trade', content=~{::#content})}"
|
th:replace="~{layout/basic_page :: layout (title='Trade', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1>Trade</h1>
|
<h1 class="display-4">Trade</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Trade currencies, cryptocurrencies, and stocks in this account's exchange.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form id="tradeForm" th:action="@{/trade/{account}(account=${data.accountId()})}" method="post">
|
<form id="tradeForm" th:action="@{/trade/{account}(account=${data.accountId()})}" method="post">
|
||||||
<input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
|
<input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
|
||||||
<input type="hidden" id="accountIdInput" th:value="${data.accountId()}"/>
|
<input type="hidden" id="accountIdInput" th:value="${data.accountId()}"/>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="sellTradeableSelect" class="form-label">Tradeable to Sell</label>
|
<label for="sellTradeableSelect" class="form-label">Asset to Sell</label>
|
||||||
<select id="sellTradeableSelect" class="form-select">
|
<select id="sellTradeableSelect" class="form-select">
|
||||||
<option selected hidden>Choose something to sell</option>
|
<option selected hidden>Choose something to sell</option>
|
||||||
<option
|
<option
|
||||||
|
@ -33,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="buyTradeableSelect" class="form-label">Tradeable to Buy</label>
|
<label for="buyTradeableSelect" class="form-label">Asset to Buy</label>
|
||||||
<select id="buyTradeableSelect" class="form-select">
|
<select id="buyTradeableSelect" class="form-select">
|
||||||
<option value="" selected disabled hidden>Choose something to buy</option>
|
<option value="" selected disabled hidden>Choose something to buy</option>
|
||||||
<option
|
<option
|
||||||
|
@ -57,7 +60,20 @@
|
||||||
<input type="hidden" name="buyTradeableId"/>
|
<input type="hidden" name="buyTradeableId"/>
|
||||||
<input type="hidden" name="value"/>
|
<input type="hidden" name="value"/>
|
||||||
|
|
||||||
<button id="submitButton" type="button" class="btn btn-primary">Submit</button>
|
<p class="text-muted">
|
||||||
|
Select an asset to sell, and an asset to buy, and then simply enter a
|
||||||
|
value that you wish to sell or buy. Note that some assets, like stocks,
|
||||||
|
may only be bought and sold in whole-number values. Also note that
|
||||||
|
prices shown in this trading interface may not be 100% accurate, and you
|
||||||
|
should consult with the information shown on your exchange's page for
|
||||||
|
the exact exchange rates.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button id="submitButton" type="button" class="btn btn-primary btn-lg">Trade</button>
|
||||||
|
|
||||||
|
<p class="text-danger fw-bold">
|
||||||
|
Warning! All trades are final, and cannot be reversed once the transaction is submitted.
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script src="/static/js/trade.js"></script>
|
<script src="/static/js/trade.js"></script>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Account', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1>
|
||||||
|
<span class="display-4" th:text="${tradeable.name()}"></span>
|
||||||
|
<small class="text-muted" th:text="${tradeable.symbol()}"></small>
|
||||||
|
</h1>
|
||||||
|
<p class="lead" th:text="${tradeable.description()}"></p>
|
||||||
|
<p>
|
||||||
|
Currently trading at a market price of
|
||||||
|
<span class="monospace" th:text="${tradeable.formattedPriceUsd()}"></span>
|
||||||
|
<strong>USD</strong> for one <strong th:text="${tradeable.symbol()}"></strong>.
|
||||||
|
</p>
|
||||||
|
<p th:if="${tradeable.exchangeId() != null}" class="text-muted">
|
||||||
|
This asset was defined in, and is only tradeable within
|
||||||
|
<a class="link-secondary" th:href="@{/exchanges/{eId}(eId=${tradeable.exchangeId()})}" th:text="${tradeable.exchangeName()}"></a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
|
@ -5,9 +5,9 @@
|
||||||
th:replace="~{layout/basic_page :: layout (title='My Profile', content=~{::#content})}"
|
th:replace="~{layout/basic_page :: layout (title='My Profile', content=~{::#content})}"
|
||||||
>
|
>
|
||||||
<div id="content" class="container">
|
<div id="content" class="container">
|
||||||
<h1>My Profile</h1>
|
<h1 class="display-4">My Profile</h1>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table table-dark">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Username</th>
|
<th scope="row">Username</th>
|
||||||
|
|
Loading…
Reference in New Issue