ui-default: init

将ui-default合并至主存储库便于维护
This commit is contained in:
undefined committed 2021-04-03 01:44:22 +08:00
1 parent 6317c9057f
commit 5faa54c7ca
564 files changed
+23083 -14

No files matched your search

@@ -0,0 +1,13 @@
.page--contest_detail
.col--status
width: rem(150px)
.col--submit-at
width: rem(150px)
.col--problem
border-left: 1px solid $table-border-color
+mobile()
.col--submit-at
display: none
@@ -0,0 +1,10 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('contest_main', () => {
// Contest Filter
$('[name="filter-form"] [name="rule"]').change(() => {
$('[name="filter-form"]').submit();
});
});
export default page;
@@ -0,0 +1,99 @@
$highlight-button-color = #F6DF45
.page--contest_main
.contest__title
font-size: rem($font-size-title)
line-height: 1.2em
margin-bottom: rem(10px)
.contest__date
margin-right: rem(10px)
white-space: nowrap
.contest__info-attended
color: $success-color
.section--contest
&.live
background: #E98696
background-image: -webkit-radial-gradient(left top, ellipse farthest-corner, #E98696 0%, #E96B6B 100%)
&.ready
background: #469DCF
background-image: -webkit-radial-gradient(left top, ellipse farthest-corner, #8ACDE6 0%, #469DCF 100%)
.status_title
display: inline-block
font-size: rem($font-size-secondary)
color: #FFF
background: rgba(#000, 0.3)
padding: rem(7px 10px)
border-radius: rem(2px)
h1
line-height: 1.2
font-size: rem(25px)
margin: rem(20px 0)
.info
color: #FFF
font-size: rem($font-size-secondary)
.icon
color: #FFEB81
font-size: rem($font-size-icon)
li
display: inline-block
margin-right: rem(10px)
.attandance
margin-top: rem(15px)
font-size: rem($font-size-secondary)
opacity: 0.8
.detail-button
cursor: pointer
display: block
text-align: center
background: $highlight-button-color
color: darken($highlight-button-color, 70%)
line-height: rem(40px)
border-radius: rem(20px)
width: rem(150px)
position: relative
overflow: hidden
transition: box-shadow .4s, background-color .4s
transition-timing-function: ease-out-cubic
&:after
position: absolute
background: #fff
content: ""
height: rem(100px)
left: rem(-70px)
top: rem(-25px)
width: rem(50px)
opacity: 0.2
transform: rotate(35deg)
transition: left 0.6s ease-out-cubic
z-index: 1
&:hover
text-decoration: none
box-shadow: 0 2px 20px rgba($highlight-button-color, 0.8)
background: lighten($highlight-button-color, 20%)
&:after
left: 150%
+mobile()
.media, .media_body, .media__right
display: block
padding: 0
.media__right .section__body
padding-top: 0
.detail-button
width: auto
@@ -0,0 +1,22 @@
.page--contest_scoreboard
.col--rank
width: rem(70px)
border-right: 1px solid $table-border-color
.col--user
width: rem(140px)
.col--total_score, .col--total_time_str, .col--solved_problems
width: rem(100px)
text-align: center
.col--problem_detail
border-left: 1px solid $table-border-color
text-align: center
+mobile()
.col--user
width: auto
.col--problem_detail
display: none
@@ -0,0 +1,10 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('discussion_detail', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
});
export default page;
@@ -0,0 +1,20 @@
.page--discussion_detail
.topic__content
margin: rem(20px 0)
.profile__bg
background-size: cover
background-position: center center
height: rem(60px)
margin-bottom: rem(-32px)
.sidebar-user-profile
.media__body
padding-left: rem(10px)
padding-top: rem(20px)
.user-profile-name
font-size: 1.2rem
color: #444
vertical-align: middle
@@ -0,0 +1,37 @@
.page--discussion_main,
.page--discussion_node,
.page--user_detail
.discussion__item
border-left: rem(5px) solid transparent
padding-left: rem(5px)
&.highlight
border-left: rem(5px) solid $highlight-color
.discussion__title
font-size: rem($font-size-title)
line-height: 1.2em
margin-bottom: rem(10px)
a:visited
color: lighten($text-1-color, 60%)
.discussion__replies
width: rem(70px)
.discussion-node__bg
height: 0
box-sizing: content-box
padding-bottom: 40%
background-size: cover
background-position: left center
background-repeat: no-repeat
for id in 'qa' 'share' 'vijos' 'solution' 'advice'
&.{id}
background-image: url('./discussion_node_bg/nodes_' + id + '.png')
+retina()
background-image: url('./discussion_node_bg/nodes@2x_' + id + '.png')
+below(rupture.desktop-cutoff)
display: none
@@ -0,0 +1,10 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('discussion_node', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
});
export default page;
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -0,0 +1,8 @@
.page--domain_join
.domain_join__container
+above(rupture.mobile-cutoff)
width: rem(500px)
margin: rem(50px) auto
+above(rupture.desktop-cutoff)
width: rem(700px)
@@ -0,0 +1,18 @@
import { NamedPage } from 'vj/misc/Page';
import * as domainEnum from 'vj/constant/domain';
const page = new NamedPage('domain_join_applications', () => {
const $role = $('[name="role"]');
const $expire = $('[name="expire"]');
const $code = $('[name="invitationCode"]');
function updateFormState() {
const method = parseInt($('[name="method"]').val(), 10);
$role.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
$expire.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
$code.prop('disabled', method !== domainEnum.JOIN_METHOD_CODE).trigger('vjFormDisableUpdate');
}
updateFormState();
$('[name="method"]').change(() => updateFormState());
});
export default page;
@@ -0,0 +1,14 @@
.page--domain_permission
.col--p
width: 80px
text-align: center
border-left: 1px solid $table-border-color
td.col--description
padding-left: rem(30px) !important
.col--family
line-height: 2
color: #FFF
background: lighten(saturate($primary-color, 70%), 30%)
border-top: 2px solid $primary-color
@@ -0,0 +1,86 @@
import _ from 'lodash';
import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
const page = new NamedPage('domain_role', () => {
const createRoleDialog = new ActionDialog({
$body: $('.dialog__body--create-role > div'),
onDispatch(action) {
const $role = createRoleDialog.$dom.find('[name="role"]');
if (action === 'ok' && $role.val() === '') {
$role.focus();
return false;
}
return true;
},
});
createRoleDialog.clear = function () {
this.$dom.find('[name="role"]').val('');
return this;
};
function ensureAndGetSelectedRoles() {
const roles = _.map(
$('.domain-roles tbody [type="checkbox"]:checked'),
(ch) => $(ch).closest('tr').attr('data-role'),
);
if (roles.length === 0) {
Notification.error(i18n('Please select at least one role to perform this operation.'));
return null;
}
return roles;
}
async function handleClickCreateRole() {
const action = await createRoleDialog.clear().open();
if (action !== 'ok') {
return;
}
const role = createRoleDialog.$dom.find('[name="role"]').val();
try {
await request.post('', {
operation: 'add',
role,
});
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
async function handleClickDeleteSelected() {
const selectedRoles = ensureAndGetSelectedRoles();
if (selectedRoles === null) return;
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm deleting the selected roles?')}</p>
<p>${i18n('Users with those roles will be removed from the domain.')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
try {
await request.post('', {
operation: 'delete',
roles: selectedRoles,
});
Notification.success(i18n('Selected roles have been deleted.'));
await delay(2000);
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
$('[name="create_role"]').click(() => handleClickCreateRole());
$('[name="delete_selected"]').click(() => handleClickDeleteSelected());
});
export default page;
@@ -0,0 +1,6 @@
.page--domain_role
.col--checkbox
width: 60px
.col--users
width: 100px
@@ -0,0 +1,157 @@
import _ from 'lodash';
import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog';
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
const page = new NamedPage('domain_user', () => {
const addUserSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--add-user [name="user"]'));
const addUserDialog = new ActionDialog({
$body: $('.dialog__body--add-user > div'),
onDispatch(action) {
const $role = addUserDialog.$dom.find('[name="role"]');
if (action === 'ok') {
if (addUserSelector.value() === null) {
addUserSelector.focus();
return false;
}
if ($role.val() === '') {
$role.focus();
return false;
}
}
return true;
},
});
addUserDialog.clear = function () {
addUserSelector.clear();
this.$dom.find('[name="role"]').val('');
return this;
};
const setRolesDialog = new ActionDialog({
$body: $('.dialog__body--set-role > div'),
onDispatch(action) {
const $role = setRolesDialog.$dom.find('[name="role"]');
if (action === 'ok' && $role.val() === '') {
$role.focus();
return false;
}
return true;
},
});
setRolesDialog.clear = function () {
this.$dom.find('[name="role"]').val('');
return this;
};
async function handleClickAddUser() {
const action = await addUserDialog.clear().open();
if (action !== 'ok') {
return;
}
const user = addUserSelector.value();
const role = addUserDialog.$dom.find('[name="role"]').val();
try {
await request.post('', {
operation: 'set_user',
uid: user._id,
role,
});
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
function ensureAndGetSelectedUsers() {
const users = _.map(
$('.domain-users tbody [type="checkbox"]:checked'),
(ch) => $(ch).closest('tr').attr('data-uid'),
);
if (users.length === 0) {
Notification.error(i18n('Please select at least one user to perform this operation.'));
return null;
}
return users;
}
async function handleClickRemoveSelected() {
const selectedUsers = ensureAndGetSelectedUsers();
if (selectedUsers === null) {
return;
}
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm removing the selected users?')}</p>
<p>${i18n('Their account will not be deleted and they will be with the default role.')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
try {
await request.post('', {
operation: 'set_users',
uid: selectedUsers,
role: 'default',
});
Notification.success(i18n('Selected users have been removed from the domain.'));
await delay(2000);
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
async function handleClickSetSelected() {
const selectedUsers = ensureAndGetSelectedUsers();
if (selectedUsers === null) {
return;
}
const action = await setRolesDialog.clear().open();
if (action !== 'ok') {
return;
}
const role = setRolesDialog.$dom.find('[name="role"]').val();
try {
await request.post('', {
operation: 'set_users',
uid: selectedUsers,
role,
});
Notification.success(i18n('Role has been updated to {0} for selected users.', role));
await delay(2000);
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
async function handleChangeUserRole(ev) {
const row = $(ev.currentTarget).closest('tr');
const role = $(ev.currentTarget).val();
try {
await request.post('', {
operation: 'set_user',
uid: row.attr('data-uid'),
role,
});
Notification.success(i18n('Role has been updated to {0}.', role));
} catch (error) {
Notification.error(error.message);
}
}
$('[name="add_user"]').click(() => handleClickAddUser());
$('[name="remove_selected"]').click(() => handleClickRemoveSelected());
$('[name="set_roles"]').click(() => handleClickSetSelected());
$('.domain-users [name="role"]').change((ev) => handleChangeUserRole(ev));
});
export default page;
@@ -0,0 +1,9 @@
.page--domain_user
.col--checkbox
width: 60px
.col--uid
width: 100px
.col--role
width: 160px
+44
View File
@@ -0,0 +1,44 @@
$error_width = 650px
$twd2_width = 270px
.page--error
.error__icon-container,
.error__text-container
text-align: center
margin: rem(20px 0)
.error__text-container
color: #777
line-height: 1.5
h1
font-size: 4rem
color: #999
margin-bottom: rem(20px)
li
list-style: decimal inside
.error__twd2
background: url('error/twd2.svg') no-repeat
width: $twd2_width
height: $twd2_width
display: inline-block
+above(rupture.mobile-cutoff)
.error__container
width: rem(700px)
margin: rem(50px) auto
position: relative
.error__icon-container
float: left
width: $twd2_width
.error__text-container
left: rem($twd2_width + 50px)
position: absolute
top: 50%
transform: translateY(-50%)
text-align: left
margin: 0
+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 270 270">
<g>
<g transform="translate(0 20)">
<path fill="#285D8F" d="M211.6 51c6.8-7.8 15-11 24.2-7.5 8.3 3 13 9.8 12 19 17 12.8 18.3 19 7.6 38.5 6 6 10.7 12.8 9.8 22.3-.2 2.3 1.7 4.8 2.6 7.2 5.2 15.4-4.6 28.2-20.6 27l-16.3-1.7c5 5.4 9 9.2 12.5 13.3 8.4 9.6 10 17.4 5.4 25.5-4.7 8.2-12.5 11.2-24.2 9.4l-6-1.5c0 2.8.2 5.2.5 7.6 1.2 9.2 0 17.4-8.6 22.6-7.7 4.7-19.5 3.3-27-3.2-3.6-3-6.6-7-10-10.6-5.5 2.4-5 7.7-6 12.2-3 13-7.2 17.6-17.2 19-9.5 1-15-2.8-20.8-14.7-.3-.6-1-1-2-2-6.5 7.6-13.2 15-24.2 15-9.4 0-15.2-6.3-18.4-20l-9.7-5.4c-13.4 8.5-22.2 10-34-6.2-9-1-18-2.2-21.5-12.6-4-11.3 1.6-19.3 12.4-26.8l-12.5 1C6 178.5-3.3 166 1 153c2.2-6.7 5.3-13 7.5-19.7 4-12 5-24.2-.8-36.3-1.7-3.5-3.4-8.7-2-11.6 3.7-7.8 9.2-14.7 13.6-22 5-8.6 12.6-11 20.2-9.6 7.2-10 13-19.3 20.3-27.4 3.4-3.7 9.5-5 16.4-8.2C88.7 3 98.8 3.7 114 23.4c8.5-9.8 10.2-10.4 26.8-9.3C145.4 3.6 154-1.7 165.5.8c9.6 2 14 10.2 15.2 20.7 9.7-4.6 19.4-7.6 27.5 1.5 7.3 8 5.3 17.7 3.4 28zM186 90.8c-17.5-.2-31 12.7-30.8 29.6 0 17.3 13.3 31 30 31 16.6 0 29.6-13.5 30-30.8.4-14-13-31.7-29.3-29.8zM84 88.6c-17.7-.2-30.7 12-31 29.3 0 17.2 13 31 29.6 31.2 16.7.2 30.3-13.4 30.4-30.4.2-17-12.4-30-29-30.2z"/>
<path fill="#549FC4" d="M258 71.4c1.6 6.7 1.8 9.6 1.8 11.7 0 2.5-.7 3.8-4 17 7.6-13.8 8.8-20.8 2-28.6zM232.5 156l14.6 1.4c16 1.3 26-11.4 20.6-27-.8-2.3-2.8-4.8-2.6-7 .7-7.3-1.8-13-5.7-17.8 2 3.7 3 7.8 2.4 12.7-.3 2.3-.4 3.6 1.2 6.7 9 17.6-5.7 28.2-10.6 30-8.4 2.8-20 1-20 1zm13 33.5c-4.6 8.2-12.4 11.2-24 9.4-2-.4-3.7-1-6.2-1.5.2 2.8.3 5.2.6 7.6 1.2 9.2 0 17.4-8.6 22.6-7.7 4.7-19.5 3.3-27-3.2-3.6-3-6.6-7-10-10.6-5.5 2.4-5 7.7-6 12.2-3 13-7.2 17.6-17.2 19-6.8.8-11.6-1-16-6.6 5.4 9.5 10.6 12.6 19.2 11.5 10-1.3 14.2-6 17-19 1-4.4.7-9.8 6.2-12.2 3.4 3.6 6.4 7.5 10 10.6 7.5 6.5 19.3 7.8 27 3.2 8.5-5.2 10-13.4 8.6-22.5-.3-2.3-.4-4.8-.6-7.5l6 1.4c11.8 1.8 19.6-1.3 24.2-9.5 4-7.3 3.2-14.3-3-22.5 3.2 6 3 11.7-.2 17.5z"/>
<g transform="translate(190 122)">
<circle r="16" fill="#111">
<animateTransform attributeName="transform" type="scale" calcMode="linear" values="1 1; 1 0; 1 1" begin="3s; loop.end + 3s" dur="0.15s" />
</circle>
</g>
<g transform="translate(87 120)">
<circle r="16" fill="#111">
<animateTransform attributeName="transform" type="scale" calcMode="linear" values="1 1; 1 0; 1 1" begin="3s; loop.end + 3s" dur="0.15s" />
</circle>
</g>
</g>
<rect id="looper" x="0" y="0" width="1" height="1" style="fill: none; stroke: none">
<set id="loop" attributeName="x" to="0" begin="0s; loop.end" dur="8s" fill="freeze"/>
</rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,5 @@
.page--home_account
[name="form_item_background_img"] .radiobox__image
width: rem(100px)
height: rem(62px)
background-size: rem(100px 62px)
@@ -0,0 +1,18 @@
.page--home_domain
.col--icon
width: 60px
line-height: 1
img
border-radius: 50%
.col--role
width: rem(160px)
border-left: 1px solid $table-border-color
.col--action
width: rem(135px)
+mobile()
.col--icon, .col--role
display: none
@@ -0,0 +1,103 @@
import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import loadReactRedux from 'vj/utils/loadReactRedux';
import parseQueryString from 'vj/utils/parseQueryString';
import { ActionDialog } from 'vj/components/dialog';
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
const page = new NamedPage('home_messages', () => {
let reduxStore;
function createDialog(user) {
reduxStore.dispatch({
type: 'DIALOGUES_CREATE',
payload: {
user,
},
});
reduxStore.dispatch({
type: 'DIALOGUES_SWITCH_TO',
payload: user._id,
});
}
async function mountComponent() {
const { default: SockJs } = await import('../components/socket');
const { default: MessagePadApp } = await import('../components/messagepad');
const { default: MessagePadReducer } = await import('../components/messagepad/reducers');
const {
React, render, Provider, store,
} = await loadReactRedux(MessagePadReducer);
reduxStore = store;
const sock = new SockJs('/home/messages-conn');
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
store.dispatch({
type: 'DIALOGUES_MESSAGE_PUSH',
payload: msg,
});
};
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]'));
const userSelectDialog = new ActionDialog({
$body: $('.dialog__body--user-select > div'),
onDispatch(action) {
if (action === 'ok' && userSelector.value() === null) {
userSelector.focus();
return false;
}
return true;
},
});
userSelectDialog.clear = function () {
userSelector.clear();
return this;
};
render(
<Provider store={store}>
<MessagePadApp
onAdd={async () => {
const action = await userSelectDialog.clear().open();
if (action !== 'ok') {
return;
}
const user = userSelector.value();
createDialog(user);
}}
/>
</Provider>,
$('#messagePad').get(0),
);
}
/**
* A target user id may be assigned in the query string.
*/
async function loadSendTarget() {
const queryString = parseQueryString();
if (!queryString.target) {
return;
}
const user = await request.get('/user/search', {
q: queryString.target,
exact_match: true,
});
if (!user || user.length === 0) {
return;
}
createDialog(user[0]);
}
async function init() {
await mountComponent();
await loadSendTarget();
}
init();
});
export default page;
@@ -0,0 +1,31 @@
.page--home_security
.sessionlist
font-size: rem($font-size-secondary)
.sessionlist__icon
font-size: rem(30px)
color: #888
margin: rem(0 20px)
.sessionlist__item
margin: rem(10px 0)
padding: rem(10px 0)
border-bottom: 1px solid #DDD
input
margin: 0
.sessionlist__current-session
line-height: rem($form-control-height)
color: green
+mobile()
.sessionlist__item
.media__left
display: none
.media__body, .media__right
display: block
padding: 0
margin: 0
@@ -0,0 +1,13 @@
.page--homework_detail
.col--status
width: rem(150px)
.col--submit-at
width: rem(150px)
.col--problem
border-left: 1px solid $table-border-color
+mobile()
.col--submit-at
display: none
@@ -0,0 +1,37 @@
import { NamedPage } from 'vj/misc/Page';
import Calendar from 'vj/components/calendar';
import i18n from 'vj/utils/i18n';
import { parse as parseMongoId } from 'vj/utils/mongoId';
const page = new NamedPage('homework_main', () => {
// Homework Calendar
if (UiContext.docs) {
const events = UiContext.docs.map((doc) => ({
beginAt: doc.beginAt,
endAt: doc.endAt,
title: doc.title,
maskFrom: doc.penaltySince ? doc.penaltySince : null,
maskTitle: i18n('Time Extension'),
colorIndex: parseMongoId(doc._id).timestamp % 12,
link: doc.url,
}));
const calendar = new Calendar(events);
calendar.getDom().appendTo('[name="calendar_entry"]');
$('[name="homework_display"]').change((ev) => {
switch (ev.currentTarget.value) {
case 'calendar':
$('.homework__list').hide();
$('[name="calendar_entry"]').show();
break;
case 'list':
$('.homework__list').show();
$('[name="calendar_entry"]').hide();
break;
default:
throw new Error('Unexpected display parameter');
}
});
}
});
export default page;
@@ -0,0 +1,14 @@
$highlight-button-color = #F6DF45
.page--homework_main
.homework__title
line-height: 1.2em
margin-bottom: rem(10px)
font-size: rem($font-size-title)
.homework__info-attended
color: $success-color
.homework__date
margin-right: rem(10px)
white-space: nowrap
@@ -0,0 +1,22 @@
.page--homework_scoreboard
.col--rank
width: rem(70px)
border-right: 1px solid $table-border-color
.col--user
width: rem(140px)
.col--total_score, .col--total_time_str, .col--solved_problems
width: rem(100px)
text-align: center
.col--problem_detail
border-left: 1px solid $table-border-color
text-align: center
+mobile()
.col--user
width: auto
.col--problem_detail
display: none
@@ -0,0 +1,34 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('manage_dashboard', async () => {
const { default: SockJs } = await import('../components/socket');
const sock = new SockJs('/manage/check-conn');
sock.onopen = () => {
$('<blockquote class="blue"><p>Connection opened.</p></blockquote>').appendTo('#messages');
};
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
if (msg.type === 'log') {
const div = $('<blockquote class="blue">').appendTo('#messages');
$(`<p>${msg.payload}</p>`).appendTo(div);
} else if (msg.type === 'warn') {
const div = $('<blockquote class="yellow">').appendTo('#messages');
$(`<p>${msg.payload}</p>`).appendTo(div);
} else if (msg.type === 'error') {
const div = $('<blockquote class="red">').appendTo('#messages');
$(`<p>${msg.payload}</p>`).appendTo(div);
}
};
sock.onclose = (message) => {
$(`
<blockquote class="yellow">
<p>Connection closed,reason=${JSON.stringify(message.reason)}</p>
</blockquote>
`).appendTo('#messages');
};
});
export default page;
@@ -0,0 +1,32 @@
import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ActionDialog } from 'vj/components/dialog';
import request from 'vj/utils/request';
const page = new NamedPage('manage_script', () => {
const runScriptDialog = new ActionDialog({
$body: $('.dialog__body--run-script > div'),
});
runScriptDialog.clear = function () {
this.$dom.find('[name="args"]').val('');
return this;
};
window.runScript = async function (id) {
const action = await runScriptDialog.clear().open();
if (action !== 'ok') return;
const args = runScriptDialog.$dom.find('[name="args"]').val();
try {
const res = await request.post('', {
args,
id,
});
window.location.href = res.url;
} catch (error) {
Notification.error(error.message);
}
};
});
export default page;
@@ -0,0 +1,30 @@
import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import delay from 'vj/utils/delay';
const page = new NamedPage('manage_user_import', () => {
async function post(draft) {
try {
const res = await request.post('', {
users: $('[name="users"]').val(),
draft,
});
if (!draft) {
Notification.success(i18n('Created {0} users.', res.users.length));
await delay(2000);
window.location.reload();
} else {
$('[name="messages"]').text(res.messages.join('\n'));
}
} catch (error) {
Notification.error(error.message);
}
}
$('[name="preview"]').click(() => post(true));
$('[name="submit"]').click(() => post(false));
});
export default page;
@@ -0,0 +1,229 @@
import { NamedPage } from 'vj/misc/Page';
import Navigation from 'vj/components/navigation';
import { ActionDialog } from 'vj/components/dialog';
import loadReactRedux from 'vj/utils/loadReactRedux';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
class ProblemPageExtender {
constructor() {
this.isExtended = false;
this.inProgress = false;
this.$content = $('.problem-content-container');
this.$contentBound = this.$content.closest('.section');
this.$scratchpadContainer = $('.scratchpad-container');
}
async extend() {
if (this.inProgress) return;
if (this.isExtended) return;
this.inProgress = true;
const bound = this.$contentBound
.get(0)
.getBoundingClientRect();
this.$content.transition({ opacity: 0 }, { duration: 100 });
await delay(100);
Navigation.instance.floating.set('scratchpad', true);
Navigation.instance.logoVisible.set('scratchpad', true);
Navigation.instance.expanded.set('scratchpad', true);
$('body').addClass('header--collapsed mode--scratchpad');
await this.$scratchpadContainer
.css({
left: bound.left,
top: bound.top,
width: bound.width,
height: bound.height,
})
.show()
.transition({
left: 0,
top: 0,
width: '100%',
height: '100%',
}, {
duration: 500,
easing: 'easeOutCubic',
})
.promise();
$('.main > .row').hide();
$('.footer').hide();
$(window).scrollTop(0);
this.inProgress = false;
this.isExtended = true;
}
async collapse() {
if (this.inProgress) return;
if (!this.isExtended) return;
this.inProgress = true;
$(window).scrollTop(0);
$('.main > .row').show();
$('.footer').show();
const bound = this.$contentBound
.get(0)
.getBoundingClientRect();
Navigation.instance.floating.set('scratchpad', false);
Navigation.instance.logoVisible.set('scratchpad', false);
Navigation.instance.expanded.set('scratchpad', false);
$('body').removeClass('header--collapsed mode--scratchpad');
await this.$scratchpadContainer
.transition({
left: bound.left,
top: bound.top,
width: bound.width,
height: bound.height,
}, {
duration: 500,
easing: 'easeOutCubic',
})
.promise();
this.$scratchpadContainer.hide();
this.$content.transition({ opacity: 1 }, { duration: 100 });
this.inProgress = false;
this.isExtended = false;
}
toggle() {
if (this.isExtended) this.collapse();
else this.extend();
}
}
const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homework_detail_problem'], () => {
let reactLoaded = false;
let renderReact = null;
let unmountReact = null;
const extender = new ProblemPageExtender();
const copyProblemDialog = new ActionDialog({
$body: $('.dialog__body--send-to > div'),
onDispatch(action) {
const $target = copyProblemDialog.$dom.find('[name="target"]');
if (action === 'ok' && $target.val() === '') {
$target.focus();
return false;
}
return true;
},
});
copyProblemDialog.clear = function () {
this.$dom.find('[name="target"]').val('');
return this;
};
async function handleClickCopyProblem() {
const action = await copyProblemDialog.clear().open();
if (action !== 'ok') return;
const target = copyProblemDialog.$dom.find('[name="target"]').val();
try {
await request.post(UiContext.postCopyUrl, {
operation: 'send',
target,
pids: [UiContext.problemNumId],
});
Notification.send(i18n('Problem Sended.'));
} catch (error) {
Notification.error(error.message);
}
}
async function scratchpadFadeIn() {
await $('#scratchpad')
.transition({
opacity: 1,
}, {
duration: 200,
easing: 'easeOutCubic',
})
.promise();
}
async function scratchpadFadeOut() {
await $('#scratchpad')
.transition({
opacity: 0,
}, {
duration: 200,
easing: 'easeOutCubic',
})
.promise();
}
async function loadReact() {
if (reactLoaded) return;
$('.loader-container').show();
const { default: SockJs } = await import('../components/socket');
const { default: ScratchpadApp } = await import('../components/scratchpad');
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
const {
React, render, unmountComponentAtNode, Provider, store,
} = await loadReactRedux(ScratchpadReducer);
const sock = new SockJs(UiContext.pretestConnUrl);
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
store.dispatch({
type: 'SCRATCHPAD_RECORDS_PUSH',
payload: msg,
});
};
renderReact = () => {
render(
<Provider store={store}>
<ScratchpadApp />
</Provider>,
$('#scratchpad').get(0),
);
};
unmountReact = () => {
unmountComponentAtNode($('#scratchpad').get(0));
};
reactLoaded = true;
$('.loader-container').hide();
}
async function enterScratchpadMode() {
await extender.extend();
await loadReact();
renderReact();
await scratchpadFadeIn();
}
async function leaveScratchpadMode() {
await scratchpadFadeOut();
$('.problem-content-container').append($('.problem-content'));
await extender.collapse();
unmountReact();
}
$(document).on('click', '[name="problem-sidebar__open-scratchpad"]', (ev) => {
enterScratchpadMode();
ev.preventDefault();
});
$(document).on('click', '[name="problem-sidebar__quit-scratchpad"]', (ev) => {
leaveScratchpadMode();
ev.preventDefault();
});
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
$('[name="problem-sidebar__send-to"]').click(() => handleClickCopyProblem());
});
export default page;
@@ -0,0 +1,175 @@
import _ from 'lodash';
import { NamedPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import tpl from 'vj/utils/tpl';
import i18n from 'vj/utils/i18n';
import { ConfirmDialog } from 'vj/components/dialog';
import Dropdown from 'vj/components/dropdown/Dropdown';
const categories = {};
const dirtyCategories = [];
const selections = [];
const tags = [];
function setDomSelected($dom, selected) {
if (selected) $dom.addClass('selected');
else $dom.removeClass('selected');
}
async function updateSelection() {
dirtyCategories.forEach(({ type, category, subcategory }) => {
let item = categories[category];
const isSelected = item.select || _.some(item.children, (c) => c.select);
setDomSelected(item.$tag, isSelected);
if (isSelected) selections.push(category);
else _.pull(selections, category);
if (type === 'subcategory') {
item = categories[category].children[subcategory];
setDomSelected(item.$tag, item.select);
const selectionName = subcategory;
if (item.select) selections.push(selectionName);
else _.pull(selections, selectionName);
}
});
const requestCategoryTags = _.uniq(selections
.filter((s) => s.indexOf(',') !== -1)
.map((s) => s.split(',')[0]));
// drop the category if its subcategory is selected
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
dirtyCategories.length = 0;
const $txt = $('[name="tag"]');
$txt.val([...requestTags, ...tags].join(', '));
}
function findCategory(name) {
const keys = Object.keys(categories);
if (keys.includes(name)) return [name, null];
for (const category of keys) {
const subkeys = Object.keys(categories[category].children);
if (subkeys.includes(name)) return [category, name];
}
return [null, null];
}
function parseCategorySelection() {
const $txt = $('[name="tag"]');
tags.length = 0;
$txt.val().split(',').map((name) => name.trim()).forEach((name) => {
if (!name) return;
const [category, subcategory] = findCategory(name);
console.log(name, category, subcategory);
if (!category) tags.push(name);
else if (!subcategory) {
categories[category].select = true;
dirtyCategories.push({ type: 'category', category });
} else if (category) {
categories[category].children[subcategory].select = true;
dirtyCategories.push({ type: 'subcategory', subcategory, category });
}
});
updateSelection();
}
function buildCategoryFilter() {
const $container = $('[data-widget-cf-container]');
if (!$container) return;
$container.attr('class', 'widget--category-filter row small-up-3 medium-up-2');
$container.children('li').get().forEach((category) => {
const $category = $(category)
.attr('class', 'widget--category-filter__category column');
const $categoryTag = $category
.find('.section__title a')
.remove()
.attr('class', 'widget--category-filter__category-tag');
const categoryText = $categoryTag.text();
const $drop = $category
.children('.chip-list')
.remove()
.attr('class', 'widget--category-filter__drop');
const treeItem = {
select: false,
$tag: $categoryTag,
children: {},
};
categories[categoryText] = treeItem;
$category.empty().append($categoryTag);
if ($drop.length > 0) {
const $subCategoryTags = $drop
.children('li')
.attr('class', 'widget--category-filter__subcategory')
.find('a')
.attr('class', 'widget--category-filter__subcategory-tag')
.attr('data-category', categoryText);
$subCategoryTags.get().forEach((subCategoryTag) => {
const $tag = $(subCategoryTag);
treeItem.children[$tag.text()] = { select: false, $tag };
});
Dropdown.getOrConstruct($categoryTag, {
target: $drop[0],
position: 'left center',
});
}
});
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
const category = $(ev.currentTarget).text();
const treeItem = categories[category];
// the effect should be cancelSelect if it is shown as selected when clicking
const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select;
treeItem.select = shouldSelect;
dirtyCategories.push({ type: 'category', category });
if (!shouldSelect) {
// de-select children
_.forEach(treeItem.children, (treeSubItem, subcategory) => {
if (treeSubItem.select) {
treeSubItem.select = false; // eslint-disable-line no-param-reassign
dirtyCategories.push({ type: 'subcategory', subcategory, category });
}
});
}
updateSelection();
ev.preventDefault();
});
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
const subcategory = $(ev.currentTarget).text();
const category = $(ev.currentTarget).attr('data-category');
const treeItem = categories[category].children[subcategory];
treeItem.select = !treeItem.select;
dirtyCategories.push({ type: 'subcategory', subcategory, category });
updateSelection();
ev.preventDefault();
});
}
const page = new NamedPage('problem_edit', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
ev.preventDefault();
if (confirmed) {
return request.post('.', { operation: 'delete' }).then((res) => {
window.location.href = res.url;
});
}
const message = 'Confirm deleting this problem? Its files, submissions, discussions and solutions will be deleted as well.';
return new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n(message)}</p>
</div>`,
}).open().then((action) => {
if (action !== 'yes') return;
confirmed = true;
ev.target.click();
});
});
$(document).on('change', '[name="tag"]', parseCategorySelection);
buildCategoryFilter();
parseCategorySelection();
});
export default page;
@@ -0,0 +1,73 @@
.page--problem_edit
.widget--category-filter
&__category
margin-bottom: rem(6px)
&-tag
display: inline-block
padding: rem(5px 6px)
&:hover
color: #FFF
background: $primary-color
text-decoration: none
&.selected
color: #FFF
background: $secondary-color
&__drop
position: relative
font-size: rem($font-size-small)
width: rem(170px)
padding: rem(10px)
margin-right: rem(10px)
background: $menu-drop-bg-color
box-shadow: $menu-drop-shadow
&:before
content: ''
display: block
position: absolute
width: 0
height: 0
border: rem($menu-drop-triangle-size) solid transparent
border-top-color: #FFF
border-left-color: #FFF
box-shadow: $menu-drop-triangle-shadow
transform: rotate(135deg)
transform-origin: 0 0
pointer-events: none
left: 100%
top: 50%
margin-left: rem(floor($menu-drop-triangle-size * 1.414))
&__subcategory
margin: rem(2px)
display: inline-block
&-tag
display: inline-block
padding: rem(5px 6px)
&:hover
color: #FFF
background: $primary-color
text-decoration: none
&.selected
color: #FFF
background: $secondary-color
.drop-target-attached-right .widget--category-filter__drop
margin-right: 0
margin-left: rem(10px)
&:before
transform-origin: 100% 0
transform: rotate(-45deg)
left: auto
right: 100%
margin-left: 0
margin-right: 0
margin-top: rem(floor($menu-drop-triangle-size * -1.414))
@@ -0,0 +1,181 @@
import { WritableStream } from 'web-streams-polyfill/dist/ponyfill.es6';
import * as streamsaver from 'streamsaver';
import _ from 'lodash';
import { createZipStream } from 'vj/utils/zip';
import { NamedPage } from 'vj/misc/Page';
import Notification from 'vj/components/notification';
import { ConfirmDialog } from 'vj/components/dialog';
import request from 'vj/utils/request';
import pipeStream from 'vj/utils/pipeStream';
import tpl from 'vj/utils/tpl';
import delay from 'vj/utils/delay';
import i18n from 'vj/utils/i18n';
try {
// Firefox have no WritableStream
if (!window.WritableStream) streamsaver.WritableStream = WritableStream;
streamsaver.mitm = '/streamsaver/mitm.html';
} catch (e) {
// Extending not allowed in snowpack
}
async function downloadProblemFilesAsArchive(type, files) {
const { links, pdoc } = await request.post('', { operation: 'get_links', files, type });
const fileStream = streamsaver.createWriteStream(`${pdoc.docId} ${pdoc.title}.zip`);
let i = 0;
const targets = [];
for (const filename of Object.keys(links)) targets.push({ filename, downloadUrl: links[filename] });
const zipStream = createZipStream({
// eslint-disable-next-line consistent-return
async pull(ctrl) {
if (i === targets.length) return ctrl.close();
try {
const response = await fetch(targets[i].downloadUrl);
if (!response.ok) throw response.statusText;
ctrl.enqueue({
name: targets[i].filename,
stream: () => response.body,
});
} catch (e) {
// eslint-disable-next-line no-use-before-define
stopDownload();
Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()]));
}
i++;
},
});
const abortCallbackReceiver = {};
function stopDownload() { abortCallbackReceiver.abort(); }
let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext;
function onBeforeUnload(e) {
if (isBeforeUnloadTriggeredByLibrary) {
isBeforeUnloadTriggeredByLibrary = false;
return;
}
e.returnValue = '';
}
window.addEventListener('unload', stopDownload);
window.addEventListener('beforeunload', onBeforeUnload);
await pipeStream(zipStream, fileStream, abortCallbackReceiver);
window.removeEventListener('unload', stopDownload);
window.removeEventListener('beforeunload', onBeforeUnload);
}
const page = new NamedPage('problem_files', () => {
function ensureAndGetSelectedFiles(type) {
const files = _.map(
$(`.problem-files-${type} tbody [data-checkbox-group="${type}"]:checked`),
(ch) => $(ch).closest('tr').attr('data-filename'),
);
if (files.length === 0) {
Notification.error(i18n('Please select at least one file to perform this operation.'));
return null;
}
return files;
}
async function handleClickUpload(type, files) {
if (!files) {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.click();
await new Promise((resolve) => { input.onchange = resolve; });
files = input.files;
}
try {
Notification.info('Uploading files...');
const tasks = [];
for (const file of files) {
const data = new FormData();
data.append('filename', file.name);
data.append('file', file);
data.append('type', type);
data.append('operation', 'upload_file');
tasks.push(request.postFile('', data));
}
await Promise.all(tasks);
Notification.success('File uploaded successfully.');
await delay(2000);
window.location.reload();
} catch (e) {
console.error(e);
Notification.error(`File upload fail: ${e.toString()}`);
}
}
async function handleClickDownloadSelected(type) {
const selectedFiles = ensureAndGetSelectedFiles(type);
if (selectedFiles === null) return;
await downloadProblemFilesAsArchive(type, selectedFiles);
}
async function handleClickRemoveSelected(type) {
const selectedFiles = ensureAndGetSelectedFiles(type);
if (selectedFiles === null) return;
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n('Confirm to delete the selected files?')}</p>
</div>`,
}).open();
if (action !== 'yes') return;
try {
await request.post('', {
operation: 'delete_files',
files: selectedFiles,
type,
});
Notification.success(i18n('Selected files have been deleted.'));
await delay(2000);
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
/**
* @param {string} type
* @param {JQuery.DragOverEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDragOver(type, ev) {
ev.preventDefault();
// TODO display a drag-drop allowed hint
}
/**
* @param {string} type
* @param {JQuery.DropEvent<HTMLElement, undefined, HTMLElement, HTMLElement>} ev
*/
function handleDrop(type, ev) {
ev.preventDefault();
ev = ev.originalEvent;
const files = [];
if (ev.dataTransfer.items) {
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
if (ev.dataTransfer.items[i].kind === 'file') {
const file = ev.dataTransfer.items[i].getAsFile();
files.push(file);
}
}
} else {
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
files.push(ev.dataTransfer.files[i]);
}
}
handleClickUpload(type, files);
}
$('.problem-files-testdata').on('dragover', (ev) => handleDragOver('testdata', ev));
$('.problem-files-additional_file').on('dragover', (ev) => handleDragOver('additional_file', ev));
$('.problem-files-testdata').on('drop', (ev) => handleDrop('testdata', ev));
$('.problem-files-additional_file').on('drop', (ev) => handleDrop('additional_file', ev));
$('[name="upload_testdata"]').on('click', () => handleClickUpload('testdata'));
$('[name="download_selected_testdata"]').on('click', () => handleClickDownloadSelected('testdata'));
$('[name="remove_selected_testdata"]').on('click', () => handleClickRemoveSelected('testdata'));
$('[name="upload_file"]').on('click', () => handleClickUpload('additional_file'));
$('[name="download_selected_file"]').on('click', () => handleClickDownloadSelected('additional_file'));
$('[name="remove_selected_file"]').on('click', () => handleClickRemoveSelected('additional_file'));
});
export default page;
@@ -0,0 +1,6 @@
.page--problem_files
.col--checkbox
width: 60px
.col--size
width: 120px
@@ -0,0 +1,6 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('problem_import', async () => {
});
export default page;
@@ -0,0 +1,168 @@
import _ from 'lodash';
import { NamedPage } from 'vj/misc/Page';
import Dropdown from 'vj/components/dropdown/Dropdown';
import pjax from 'vj/utils/pjax';
import substitute from 'vj/utils/substitute';
const categories = {};
const dirtyCategories = [];
const selections = [];
function setDomSelected($dom, selected) {
if (selected) {
$dom.addClass('selected');
} else {
$dom.removeClass('selected');
}
}
async function updateSelection(sendRequest = true) {
dirtyCategories.forEach(({ type, category, subcategory }) => {
let item = categories[category];
const isSelected = item.select || _.some(item.children, (c) => c.select);
setDomSelected(item.$tag, isSelected);
if (isSelected) {
selections.push(category);
} else {
_.pull(selections, category);
}
if (type === 'subcategory') {
item = categories[category].children[subcategory];
setDomSelected(item.$tag, item.select);
const selectionName = subcategory;
if (item.select) {
selections.push(selectionName);
} else {
_.pull(selections, selectionName);
}
}
});
dirtyCategories.length = 0;
if (sendRequest) {
// a list of categories which subcategory is selected
const requestCategoryTags = _.uniq(selections
.filter((s) => s.indexOf(',') !== -1)
.map((s) => s.split(',')[0]));
// drop the category if its subcategory is selected
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
let url;
if (requestTags.length === 0) {
url = UiContext.getProblemUrlWithoutCategory;
} else {
url = substitute(decodeURIComponent(UiContext.getProblemUrlWithCategory), {
category: requestTags
.map((tag) => tag.split(',').map(encodeURIComponent).join(','))
.join('+'), // build a beautiful URL
});
}
pjax.request({ url });
}
}
function buildCategoryFilter() {
const $container = $('[data-widget-cf-container]');
if (!$container) return;
$container.attr('class', 'widget--category-filter row small-up-3 medium-up-2');
$container.children('li').get().forEach((category) => {
const $category = $(category)
.attr('class', 'widget--category-filter__category column');
const $categoryTag = $category
.find('.section__title a')
.remove()
.attr('class', 'widget--category-filter__category-tag');
const categoryText = $categoryTag.text();
const $drop = $category
.children('.chip-list')
.remove()
.attr('class', 'widget--category-filter__drop');
const treeItem = {
select: false,
$tag: $categoryTag,
children: {},
};
categories[categoryText] = treeItem;
$category.empty().append($categoryTag);
if ($drop.length > 0) {
$categoryTag.text(`${$categoryTag.text()}`);
const $subCategoryTags = $drop
.children('li')
.attr('class', 'widget--category-filter__subcategory')
.find('a')
.attr('class', 'widget--category-filter__subcategory-tag')
.attr('data-category', categoryText);
$subCategoryTags.get().forEach((subCategoryTag) => {
const $tag = $(subCategoryTag);
treeItem.children[$tag.text()] = {
select: false,
$tag,
};
});
Dropdown.getOrConstruct($categoryTag, {
target: $drop[0],
position: 'left center',
});
}
});
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) {
return;
}
const category = $(ev.currentTarget).text();
const treeItem = categories[category];
// the effect should be cancelSelect if it is shown as selected when clicking
const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select;
treeItem.select = shouldSelect;
dirtyCategories.push({ type: 'category', category });
if (!shouldSelect) {
// de-select children
_.forEach(treeItem.children, (treeSubItem, subcategory) => {
if (treeSubItem.select) {
treeSubItem.select = false; // eslint-disable-line no-param-reassign
dirtyCategories.push({ type: 'subcategory', subcategory, category });
}
});
}
updateSelection();
ev.preventDefault();
});
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) {
return;
}
const subcategory = $(ev.currentTarget).text();
const category = $(ev.currentTarget).attr('data-category');
const treeItem = categories[category].children[subcategory];
treeItem.select = !treeItem.select;
dirtyCategories.push({ type: 'subcategory', subcategory, category });
updateSelection();
ev.preventDefault();
});
}
function parseCategorySelection() {
UiContext.currentCategory.split(' ').forEach((cline) => {
const [category, subcategory] = cline.split(',');
if (!categories[category]) {
return;
}
if (subcategory && !categories[category].children[subcategory]) {
return;
}
if (!subcategory) {
categories[category].select = true;
dirtyCategories.push({ type: 'category', category });
} else {
categories[category].children[subcategory].select = true;
dirtyCategories.push({ type: 'subcategory', subcategory, category });
}
});
updateSelection(false);
}
const page = new NamedPage(['problem_main', 'problem_category'], () => {
buildCategoryFilter();
parseCategorySelection();
});
export default page;
@@ -0,0 +1,99 @@
.page--problem_main
.page--problem_category
.page--problem_settings
.col--status
width: rem(150px)
position: relative
.col--submit_n, .col--ac_rate, .col--difficulty
width: rem(70px)
text-align: center
.col--status
border-right: 1px solid $table-border-color
.col--submit_n, .col--difficulty
border-left: 1px solid $table-border-color
+mobile()
.col--status,
.col--submit_n,
.col--difficulty
display: none
.col--ac_rate
border-left: 1px solid $table-border-color
.widget--category-filter
&__category
margin-bottom: rem(6px)
&-tag
display: inline-block
padding: rem(5px 6px)
&:hover
color: #FFF
background: $primary-color
text-decoration: none
&.selected
color: #FFF
background: $secondary-color
&__drop
position: relative
font-size: rem($font-size-small)
width: rem(170px)
padding: rem(10px)
margin-right: rem(10px)
background: $menu-drop-bg-color
box-shadow: $menu-drop-shadow
&:before
content: ''
display: block
position: absolute
width: 0
height: 0
border: rem($menu-drop-triangle-size) solid transparent
border-top-color: #FFF
border-left-color: #FFF
box-shadow: $menu-drop-triangle-shadow
transform: rotate(135deg)
transform-origin: 0 0
pointer-events: none
left: 100%
top: 50%
margin-left: rem(floor($menu-drop-triangle-size * 1.414))
&__subcategory
margin: rem(2px)
display: inline-block
&-tag
display: inline-block
padding: rem(5px 6px)
&:hover
color: #FFF
background: $primary-color
text-decoration: none
&.selected
color: #FFF
background: $secondary-color
.drop-target-attached-right .widget--category-filter__drop
margin-right: 0
margin-left: rem(10px)
&:before
transform-origin: 100% 0
transform: rotate(-45deg)
left: auto
right: 100%
margin-left: 0
margin-right: 0
margin-top: rem(floor($menu-drop-triangle-size * -1.414))
@@ -0,0 +1,10 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('problem_solution', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
});
export default page;
@@ -0,0 +1,10 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('problem_submit', async () => {
$(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
$(ev.currentTarget).hide();
$('[name="problem-sidebar__categories"]').show();
});
});
export default page;
@@ -0,0 +1,12 @@
.page--problem_submit
.col--status
width: rem(150px)
.col--submit-at
width: rem(160px)
text-align: center
+mobile()
.col--memory,
.col--time
display: none
@@ -0,0 +1,16 @@
.page--ranking
.col--rank
width: rem(70px)
position: relative
.col--user
width: rem(200px)
text-align: center
.col--rp
width: rem(80px)
text-align: right
+mobile()
.col--bio
display: none
@@ -0,0 +1,20 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('record_detail', async () => {
const { default: SockJs } = await import('../components/socket');
const { DiffDOM } = await import('diff-dom');
const sock = new SockJs(UiContext.socketUrl);
const dd = new DiffDOM();
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
const newStatus = $(msg.status_html);
const oldStatus = $('#status');
dd.apply(oldStatus[0], dd.diff(oldStatus[0], newStatus[0]));
const newSummary = $(msg.summary_html);
const oldSummary = $('#summary');
dd.apply(oldSummary[0], dd.diff(oldSummary[0], newSummary[0]));
};
});
export default page;
@@ -0,0 +1,26 @@
.page--record_detail
.compiler-text
padding: rem(20px 0)
font-size: rem($font-size-small)
&:empty
display: none
.col--case
width: rem(70px)
.col--memory, .col--time
width: rem(120px)
.col--case, .col--status
border-right: 1px solid $table-border-color
+mobile()
.col--case
display: none
.col--time
width: rem(70px)
.col--memory
width: rem(100px)
@@ -0,0 +1,32 @@
import { NamedPage } from 'vj/misc/Page';
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
const page = new NamedPage('record_main', async () => {
const { default: SockJs } = await import('../components/socket');
const { DiffDOM } = await import('diff-dom');
const sock = new SockJs(UiContext.socketUrl);
const dd = new DiffDOM();
sock.onopen = () => {
sock.send(JSON.stringify({ rids: UiContext.rids }));
};
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
const $newTr = $(msg.html);
const $oldTr = $(`.record_main__table tr[data-rid="${$newTr.attr('data-rid')}"]`);
if ($oldTr.length) {
$oldTr.trigger('vjContentRemove');
dd.apply($oldTr[0], dd.diff($oldTr[0], $newTr[0]));
$oldTr.trigger('vjContentNew');
} else {
$('.record_main__table tbody').prepend($newTr);
$newTr.trigger('vjContentNew');
}
};
UserSelectAutoComplete.getOrConstruct($('.filter-user [name="uidOrName"]'), {
clearDefaultValue: false,
});
});
export default page;
@@ -0,0 +1,55 @@
.page--record_main
.col--status
width: rem(170px)
position: relative
.col--submit-at
width: rem(120px)
text-align: center
&.nojs .col--submit-at
width: rem(160px)
.col--submit-by
width: rem(120px)
text-align: right
.col--time, .col--memory, .col--lang
width: rem(100px)
text-align: center
.col--status, .col--submit-by, .col--lang
border-right: 1px solid $table-border-color
.col--status__text
position: relative
z-index: 2
.col--status__progress-container
position: absolute
left: 0
top: 0
width: 100%
height: 100%
z-index: 1
.col--status__progress
position: absolute
left: 0
top: 0
height: 100%
background: #fff6d1
+tablet()
.col--time,
.col--memory,
.col--lang
display: none
+mobile()
.col--submit-by,
.col--submit-at,
.col--time,
.col--memory,
.col--lang
display: none
@@ -0,0 +1,17 @@
.page--status
.col--id
width: 70px
.col--status
width: 100px
.col--memory
width: 150px
.col--request
width: 100px
+mobile()
.col--os
.col--cpu
display: none
@@ -0,0 +1,35 @@
import { NamedPage } from 'vj/misc/Page';
import { slideDown, slideUp } from 'vj/utils/slide';
async function handleSectionExpand(ev) {
const $section = $(ev.currentTarget).closest('.training__section');
if ($section.is('.expanded, .animating')) {
return;
}
$section.addClass('animating');
const $detail = $section.find('.training__section__detail');
await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 });
$section.addClass('expanded');
$section.removeClass('collapsed');
$section.removeClass('animating');
}
async function handleSectionCollapse(ev) {
const $section = $(ev.currentTarget).closest('.training__section');
if ($section.is('.collapsed, .animating')) {
return;
}
$section.addClass('animating');
const $detail = $section.find('.training__section__detail');
await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 });
$section.addClass('collapsed');
$section.removeClass('expanded');
$section.removeClass('animating');
}
const page = new NamedPage('training_detail', async () => {
$(document).on('click', '[name="training__section__expand"]', handleSectionExpand);
$(document).on('click', '[name="training__section__collapse"]', handleSectionCollapse);
});
export default page;
@@ -0,0 +1,35 @@
.page--training_detail
.col--status
width: rem(150px)
position: relative
.col--submit_n, .col--ac_rate, .col--difficulty
width: rem(70px)
text-align: center
.col--status
border-right: 1px solid $table-border-color
.col--submit_n, .col--difficulty
border-left: 1px solid $table-border-color
+mobile()
.col--status,
.col--submit_n,
.col--difficulty
display: none
.col--ac_rate
border-left: 1px solid $table-border-color
.training__section
&.invalid
.training__problems
opacity: 0.6
&.collapsed .collapsed--hidden,
&.expanded .expanded--hidden
display: none
&.collapsed .training__section__detail
display: none
@@ -0,0 +1,28 @@
import { NamedPage } from 'vj/misc/Page';
import { ConfirmDialog } from 'vj/components/dialog';
import tpl from 'vj/utils/tpl';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
export default new NamedPage('training_edit', () => {
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
ev.preventDefault();
if (confirmed) {
return request.post('.', { operation: 'delete' }).then((res) => {
window.location.href = res.url;
});
}
const message = 'Confirm deleting this training? Its status will be deleted as well.';
return new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n(message)}</p>
</div>`,
}).open().then((action) => {
if (action !== 'yes') return;
confirmed = true;
ev.target.click();
});
});
});
@@ -0,0 +1,48 @@
.page--training_main
.training__title
line-height: 1.2
margin-bottom: 0.4em
font-size: rem($font-size-title)
.training__intro
color: lighten($text-1-color, 20%)
font-size: rem($font-size-secondary)
margin: 1em 0
.training__item
padding-left: rem(5px)
.training__participants
width: rem(80px)
.training__progress
line-height: rem(17px)
vertical-align: top
.training__progress-bar
display: inline-block
width: rem(100px)
height: rem(5px)
position: relative
background: #EEE
margin: rem(6px 0)
line-height: 1
vertical-align: top
.training__progress-track
position: absolute
left: 0
top: 0
height: 100%
width: 0%
background-color: green
.secondary.training__list
.training__item
margin-bottom: rem(15px)
.training__title
font-size: rem($font-size)
&.my
.training-status--icon
font-size: rem($font-size-icon * 1.2)
@@ -0,0 +1,23 @@
import base64 from 'base-64';
import Clipboard from 'clipboard';
import { NamedPage } from 'vj/misc/Page';
import substitute from 'vj/utils/substitute';
import Notification from 'vj/components/notification';
import i18n from 'vj/utils/i18n';
const page = new NamedPage('user_detail', async () => {
$('[name="profile_contact_copy"]').get().forEach((el) => {
const data = $(el).attr('data-content');
const decoded = base64.decode(data);
const clip = new Clipboard(el, { text: () => decoded });
clip.on('success', () => {
Notification.success(substitute(i18n('"{data}" copied to clipboard!'), { data: decoded }), 2000);
});
clip.on('error', () => {
Notification.error(substitute(i18n('Copy "{data}" failed :('), { data: decoded }));
});
});
});
export default page;
@@ -0,0 +1,90 @@
@import '~vj/components/tab/var.inc.styl'
.page--user_detail
.profile-header
position: relative
background-position: center center
background-size: cover
min-height: rem(270px)
padding-bottom: rem($tab-item-height + $tab-item-shadow-size + 20px)
+mobile()
padding-bottom: rem($tab-item-height-mobile)
.profile-header__content
position: relative
padding: rem($section-gap-v) rem($section-gap-h) 0
.media__left
padding-right: rem(30px)
+mobile()
.media, .media__left, .media__body
display: block
.media__left
text-align: center
img
display: inline-block
.media__body
margin-top: rem(20px)
.profile-header__contact-bar
position: absolute
right: rem($section-gap-h + 5px)
top: rem($section-gap-v + 20px)
color: #FFF
text-align: right
+below(rupture.hd-cutoff)
position: relative
right: 0
top: 0
text-align: left
margin: rem(15px 0)
.profile-header__contact-item
vertical-align: top
color: rgba(#FFF, 0.8)
margin-left: rem(10px)
+below(rupture.hd-cutoff)
margin-left: 0
margin-right: rem(15px)
&:hover
color: rgba(#FFF, 1)
text-decoration: none
&.badge--mod
background-color: rgba(#FFF, 0.8) !important
color: rgba(#000, 0.8) !important
.profile-header__main
h1
line-height: 40px
color: #FFF
p
line-height: 30px
color: #FFF
small
color: rgba(#FFF, 0.8)
.profile-content
position: relative
z-index: 2
margin-top: rem(-($tab-item-height + $tab-item-shadow-size))
+mobile()
margin-top: rem(-($tab-item-height-mobile))
.section__tabs
padding-top: rem($tab-item-height + $tab-item-shadow-size)
+mobile()
padding-top: rem($tab-item-height-mobile)